tcaicc 1.4.0rc1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tcaicc-1.4.0rc1/.gitignore +30 -0
- tcaicc-1.4.0rc1/PKG-INFO +216 -0
- tcaicc-1.4.0rc1/README.md +195 -0
- tcaicc-1.4.0rc1/doc/demo.py +47 -0
- tcaicc-1.4.0rc1/doc/demo_endpoint.py +48 -0
- tcaicc-1.4.0rc1/doc/demo_endpoint_stream.py +62 -0
- tcaicc-1.4.0rc1/doc/demo_stream.py +61 -0
- tcaicc-1.4.0rc1/pyproject.toml +96 -0
- tcaicc-1.4.0rc1/src/tcaicc/__init__.py +120 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/__init__.py +1 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/cipherx/__init__.py +151 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/cipherx/_aead.py +43 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/cipherx/_binding.py +208 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/cipherx/_constants.py +17 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/cipherx/_decrypt.py +109 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/cipherx/_encrypt.py +127 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/cipherx/_envelope.py +264 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/cipherx/_errors.py +67 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/cipherx/_hash.py +14 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/cipherx/_kat.py +82 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/cipherx/_metadata.py +141 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/cipherx/_padding.py +68 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/cipherx/_prefix.py +11 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/cipherx/_requestid.py +26 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/cipherx/_stream.py +189 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/cryptox/__init__.py +54 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/cryptox/_aead.py +43 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/cryptox/_dek.py +36 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/cryptox/_errors.py +22 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/cryptox/_kdf.py +36 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/cryptox/_kex.py +73 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/cryptox/_lp.py +17 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/cryptox/_mac.py +13 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/cryptox/_nonce.py +7 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/cryptox/_subtle.py +10 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/__init__.py +183 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/_body.py +242 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/_client.py +1091 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/_crypto_errors.py +58 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/_derive.py +59 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/_detect.py +33 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/_endpoint_pool.py +149 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/_endpoints.py +29 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/_errors.py +53 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/_field_path_aliases.py +86 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/_flags.py +12 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/_http.py +300 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/_keysched.py +95 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/_log.py +166 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/_model_pool.py +315 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/_policy.py +179 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/_pool.py +310 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/_proto.py +74 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/_request.py +221 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/_sev.py +254 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/_stream.py +93 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/_tls.py +200 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/_transcript.py +116 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/_transport.py +80 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/_trust_anchors.py +127 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/trust_anchors/bergamo_ark.pem +37 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/trust_anchors/bergamo_ask.pem +37 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/trust_anchors/genoa_ark.pem +37 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/trust_anchors/genoa_ask.pem +37 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/trust_anchors/milan_ark.pem +37 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/trust_anchors/milan_ask.pem +37 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/trust_anchors/siena_ark.pem +37 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/trust_anchors/siena_ask.pem +37 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/trust_anchors/turin_ark.pem +37 -0
- tcaicc-1.4.0rc1/src/tcaicc/common/security/trust_anchors/turin_ask.pem +37 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Byte-compiled / optimized
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Packaging
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
.eggs/
|
|
11
|
+
|
|
12
|
+
# Virtual environments
|
|
13
|
+
.venv/
|
|
14
|
+
venv/
|
|
15
|
+
|
|
16
|
+
# Test / coverage caches
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.coverage
|
|
19
|
+
.coverage.*
|
|
20
|
+
htmlcov/
|
|
21
|
+
|
|
22
|
+
# Editors / OS
|
|
23
|
+
.idea/
|
|
24
|
+
.vscode/
|
|
25
|
+
.DS_Store
|
|
26
|
+
|
|
27
|
+
# 明文默认 mTLS 证书源材料(由 hardcode.sh 加密进 _default_certs.py;密文可提交,明文不提交)
|
|
28
|
+
cert/
|
|
29
|
+
|
|
30
|
+
# doc/ 下 demo*.py 是权威文档示例,随仓库提交;doc.html 不在本仓库,维护于 ../aicc-sdk-doc/。
|
tcaicc-1.4.0rc1/PKG-INFO
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tcaicc
|
|
3
|
+
Version: 1.4.0rc1
|
|
4
|
+
Summary: Tencent AICC confidential-model SDK for Python
|
|
5
|
+
Author: Tencent
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Keywords: aicc,confidential-computing,encryption,ml-kem,remote-attestation,sev-snp,tencent
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Security :: Cryptography
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: cryptography>=48
|
|
19
|
+
Requires-Dist: httpx>=0.27
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# tcaicc
|
|
23
|
+
|
|
24
|
+
Tencent AICC 机密模型的 Python SDK。
|
|
25
|
+
|
|
26
|
+
接入分两步:
|
|
27
|
+
|
|
28
|
+
1. **建立安全会话** —— 与运行在 SEV-SNP TEE 中的 aicc-service 握手,取得两端一致的 32B SessionKey(含远程证明验证)。推荐用 `SessionPool` 托管会话生命周期。
|
|
29
|
+
2. **加密的 LLM 调用** —— 用该 SessionKey 加密 prompt、解密网关回文。加解密全程在本地完成,网关只见密文。
|
|
30
|
+
|
|
31
|
+
完整文档见 **[`doc/doc-python.html`](doc/doc-python.html)**(用法说明 + 原理简介);可运行示例分两套:按 model 经 `ModelPool` 发现端点的 [`doc/demo.py`](doc/demo.py)(非流式)/ [`doc/demo_stream.py`](doc/demo_stream.py)(流式),与直接指定端点的 [`doc/demo_endpoint.py`](doc/demo_endpoint.py)(非流式)/ [`doc/demo_endpoint_stream.py`](doc/demo_endpoint_stream.py)(流式,额外需 `AICC_ENDPOINT`)。
|
|
32
|
+
|
|
33
|
+
## 安装
|
|
34
|
+
|
|
35
|
+
要求 Python ≥ 3.10。
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bash install.sh # 从源码构建并安装(覆盖旧版)
|
|
39
|
+
bash install.sh --prod xxx.whl # 安装指定 wheel
|
|
40
|
+
pip install tcaicc # 或从私有源安装
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
运行时依赖 `cryptography`(≥48)、`httpx`(≥0.27);测试依赖 `pytest`、`ruff`。
|
|
44
|
+
|
|
45
|
+
## 快速开始
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import json
|
|
49
|
+
import os
|
|
50
|
+
|
|
51
|
+
import httpx
|
|
52
|
+
|
|
53
|
+
from tcaicc import ModelPool, SessionFactory, SessionPool, default_policy, Protocol
|
|
54
|
+
|
|
55
|
+
base_url = os.getenv("AICC_BASE_URL") or "https://aicc.example.com/v1/aicc"
|
|
56
|
+
gateway_url = os.getenv("AICC_GATEWAY_URL") or "https://aicc.example.com/v1/chat/completions"
|
|
57
|
+
api_key = os.getenv("AICC_API_KEY")
|
|
58
|
+
model_name = os.getenv("AICC_MODEL_NAME") or "your-model"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def build(endpoint): # 每端点一个独立工厂;端点由 ModelPool 统一注入
|
|
62
|
+
return SessionPool(
|
|
63
|
+
SessionFactory(base_url=base_url, ttl=60 * 60 * 8, api_key=api_key),
|
|
64
|
+
4, # num = 池容量(会话数),非并发上限
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
with ModelPool(base_url, api_key, 60 * 60 * 8, build) as models:
|
|
69
|
+
req = models.new_request(model_name) # 发现+绑定+取会话+本轮请求(每轮一个)
|
|
70
|
+
policy = default_policy(Protocol.OPENAI) # 内置默认路径,与网关互锚
|
|
71
|
+
body = {"model": model_name, "messages": [{"role": "user", "content": "你好"}]}
|
|
72
|
+
enc_body = req.encrypt_request(json.dumps(body).encode(), policy) # 按 policy 路径加密整个 body
|
|
73
|
+
|
|
74
|
+
r = httpx.post(gateway_url, content=enc_body,
|
|
75
|
+
headers={"Content-Type": "application/json",
|
|
76
|
+
"X-AICC-Mode": "1",
|
|
77
|
+
"Authorization": f"Bearer {api_key}",
|
|
78
|
+
"X-AICC-Endpoint": req.endpoint}) # 握手时的集群端点,网关据此路由
|
|
79
|
+
plain = req.decrypt_response(r.content, policy) # 按 policy 路径解密整个响应
|
|
80
|
+
print(json.loads(plain)["choices"][0]["message"]["content"])
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
> **每轮 HTTP 请求必须新建一个 `Request`**:同一请求内所有密文段须出自同一个 `Request`,否则网关因凭证不一致返回 400。
|
|
84
|
+
|
|
85
|
+
> `set_api_key` 只展开 `Authorization: Bearer`(不再带 `X-AICC-Mode`)。
|
|
86
|
+
|
|
87
|
+
`req.encrypt` / `req.decrypt` 为 raw 级(原始信封字节,base64 由调用方处理,须显式给位置);`req.encrypt_request` / `req.decrypt_response` 为 JSON 结构级(内部 base64,按 `Policy` 路径处理整个 body,位置自算)。两者示例见下节。
|
|
88
|
+
|
|
89
|
+
### 逐段加解密(raw)与整体加解密
|
|
90
|
+
|
|
91
|
+
整体(按 `Policy` 路径批量处理整个 body / 响应,位置由 SDK 自算)——即上文快速开始用法:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from tcaicc import default_policy, Protocol
|
|
95
|
+
|
|
96
|
+
policy = default_policy(Protocol.OPENAI) # 内置默认路径
|
|
97
|
+
enc_body = req.encrypt_request(json.dumps(body).encode(), policy)
|
|
98
|
+
plain = req.decrypt_response(resp_bytes, policy)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
逐段(raw,精确控制单字段)——返回/接收原始信封字节,调用方自行 base64 并置入 JSON,须显式给位置(`field_path` = 该字段的 policy 路径、`seq` = 同路径内第几段,从 0 起):
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
import base64
|
|
105
|
+
|
|
106
|
+
# 加密:密文信封 base64 后置入请求字段
|
|
107
|
+
body = {
|
|
108
|
+
"model": model_name,
|
|
109
|
+
"messages": [{"role": "user",
|
|
110
|
+
"content": base64.b64encode(
|
|
111
|
+
req.encrypt("你好".encode(), "messages.content", 0)).decode()}],
|
|
112
|
+
}
|
|
113
|
+
# 解密:从响应字段取出 base64 → 信封字节 → 明文
|
|
114
|
+
field = r.json()["choices"][0]["message"]["content"]
|
|
115
|
+
plain = req.decrypt(base64.b64decode(field), "choices.message.content", 0).decode()
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 流式响应(SSE)
|
|
119
|
+
|
|
120
|
+
流式响应由调用方自行按 SSE 切分事件,逐事件交给 `StreamDecryptor` 解密。`decrypt_event` 按 `policy` 自动定位密文段(与 `decrypt_response` 同一套路径),返回 `(事件 JSON, is_last)`;返回 `None` 表示该事件是网关合成的尾片(勿转发),零命中 / 非 JSON 事件(如 `[DONE]`)原样返回事件字节,`is_last` 收到合法尾片后恒为 `True`。**只有 Strict 有尾片**:默认 Compatible / Simple 对新 gateway 不发尾片、`is_last` 恒 `False`,以协议终止帧(`[DONE]` / `message_stop` / Responses 终止事件)+ EOF 收束,且只有逐片完整性(丢片 / 截断不可察),要整流完整性用 `Mode.STRICT`。同一 `Request` 勿并发多条流;Compatible / Simple 下流结束后 gateway 异步释放槽位,勿把同一 `Request` 立刻用于下一次 HTTP 请求。非流式 `decrypt_response` 缺尾片只在 Strict 报 `ErrStreamTruncated`(`tcaicc.common.security`)。
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
sd = req.new_stream_decryptor()
|
|
124
|
+
with httpx.stream("POST", gateway_url, content=enc_body,
|
|
125
|
+
headers={"Content-Type": "application/json",
|
|
126
|
+
"X-AICC-Mode": "1",
|
|
127
|
+
"Authorization": f"Bearer {api_key}",
|
|
128
|
+
"X-AICC-Endpoint": req.endpoint}) as r:
|
|
129
|
+
for line in r.iter_lines():
|
|
130
|
+
if not line.startswith("data:"):
|
|
131
|
+
continue
|
|
132
|
+
data = line[len("data:"):].strip()
|
|
133
|
+
if data == "[DONE]":
|
|
134
|
+
break
|
|
135
|
+
plain, is_last = sd.decrypt_event(data.encode(), policy)
|
|
136
|
+
choices = json.loads(plain).get("choices") if plain else None
|
|
137
|
+
if choices: # usage 等事件 choices 为空
|
|
138
|
+
print(choices[0].get("delta", {}).get("content") or "", end="")
|
|
139
|
+
if is_last: # 对新 gateway 仅 Strict 会翻 True
|
|
140
|
+
break
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 长度隐藏
|
|
144
|
+
|
|
145
|
+
prompt 与非流式 response 的内层按 1.5 倍几何桶填充(256 B 起,顶桶 9,695,112,共 27 桶),中间层只见所在的桶、不见精确长度;`pl` 即桶长,Hash 仍只盖真实数据。单段明文 ≤ `MAX_PADDED_PLAINTEXT_LEN`(9,695,108 B),超出抛 `TooLargeError`;`padded_len(n)` 可预估填充后长度。流式 chunk 不填充,片长仍可见;非流式回文仅在中间层逐字节透传时成立。
|
|
146
|
+
|
|
147
|
+
### 会话池 / 端点池 / 模型池
|
|
148
|
+
|
|
149
|
+
- `SessionPool(factory, num, key_generator=None)`:惰性托管至多 `num` 个共享会话;`get()` 取一个共享会话(勿 `close`),`new_request()` 一步取本轮请求(取到即被淘汰时自动重取)。`set_endpoint()` / `get_endpoint()` 转调工厂。默认 derive 模式,传 `key_generator`(返回 32B)走 import:会话经 `factory.new_session_with_key_generator(key_gen)` 建立,建会话与该会话每次 `renew()` 都重调一次(长度不符 `CryptoKeySizeError`(security 包的 `KeySizeError`,顶层别名)、回调抛错 → `KeyGeneratorError`(`"aicc sdk: key generator: <原错误>"`,原异常在 `__cause__`,无错误码),均在出网前;key 用后随机重写);`new_session(key)` 固定 key 的会话 `renew` 重导入同一把;同一会话并发 `renew` 串行化。
|
|
150
|
+
- `EndpointPool(ttl)`:`endpoint → SessionPool` 固定寿命托管(`ttl` 秒,`-1` 永不过期);`set/get/delete/close`,到期或替换即关旧池。
|
|
151
|
+
- `ModelPool(base_url, api_key, ttl, build)`:按 `model+flags` 经 `describe_endpoints` 发现集群端点、绑定期 `ttl`、引用计数、刷新失败沿用旧绑定;`build(endpoint) -> SessionPool` 须每端点新建独立工厂。
|
|
152
|
+
- 集群端点:`describe_endpoints` 返回不透明句柄(形如 `ept-…`),经 `factory.set_endpoint()` 于握手时以 `X-AICC-Endpoint` 头发出(不校验、不入 transcript);`session.get_endpoint()` / `request.endpoint` 读回该路由值。
|
|
153
|
+
- flags 词表:`FLAG_PROMPT_SHORT` / `FLAG_PROMPT_LONG`(路由提示,服务端本期不解析)。
|
|
154
|
+
- 端点发现失败(服务端端点密钥未配等)抛 `SecuritySessionError`,`code == ENDPOINT_NOT_CONFIGURED`;不影响握手与数据面。
|
|
155
|
+
- 默认位置校验模式为 **Compatible**(位置/协议不符记 info 日志后照解,默认阈值下不可见);要拒绝不符显式 `factory.set_mode(Mode.STRICT)`(`Mode` 自 `tcaicc.common.cipherx` 导入)。
|
|
156
|
+
- 握手与 `describe_endpoints` 每次尝试 15 秒超时(重试各自计时,默认传输施加);`CreateSecuritySession` 与 `describe_endpoints` 遇传输失败 / 超时 / 5xx 有限重试(最多再试 2 次,退避 200ms、400ms,同一 RequestId);业务错误与 Import / Derive 一次性消费不重试;单次响应上限 1 MiB。`set_transport` 注入的函数返回 None 或抛异常视同网络错误(文案含 `transport returned nil response` / 原异常),按上述规则重试。
|
|
157
|
+
|
|
158
|
+
### 日志
|
|
159
|
+
|
|
160
|
+
日志是**进程全局**的,没有按对象的设置项(对齐 Go `sdk.SetLogger` / `sdk.SetLogLevel`,spec `2026-10-10-sdk-logging-levels-design.md`):
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
import tcaicc
|
|
164
|
+
|
|
165
|
+
tcaicc.set_logger(my_logger) # 实现 debugf(msg) / infof(msg) / errorf(msg) 三方法即可;None 恢复默认(stderr)
|
|
166
|
+
tcaicc.set_log_level(tcaicc.LogLevel.INFO) # 阈值 DEBUG < INFO < ERROR < NONE;默认 ERROR;非法值 no-op
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
- 四级:`debug` 每请求 / 每次尝试的细节(握手每个 Action 的成功与重试、发现结果、请求创建、加解密命中数、池新建决策);`info` 生命周期与被容忍 / 吞掉的事件(建会话、续约、会话入池出池、端点绑定换绑释放、Compatible / Simple 容忍的不一致、聚合分支、建会话失败回落、刷新失败沿用旧绑定、未钉 ARK);`error` 当前没有落点;`none` 不落任何日志。
|
|
170
|
+
- **默认阈值 error,而 SDK 当前没有 error 级落点,故默认零输出**;要看容忍 / 回落 / 生命周期开 info,看每请求细节开 debug。
|
|
171
|
+
- logger 收到的是已格式化的一行文本,全部以 `aicc sdk: ` 开头,不含明文、密钥、api_key、token。默认实现写 stderr,前缀 `[DEBUG] ` / `[INFO] ` / `[ERROR] `。
|
|
172
|
+
- 两个设置立即生效,对已建的工厂 / 池 / 会话 / 请求 / 流一视同仁;logger 须线程安全、勿阻塞、勿抛异常。SDK 不在自身锁内调用 logger(`renew` 期间握手层日志除外),但 logger 内勿同步等待自己可能正处于其中的 SDK 工作:同一会话的 `renew`、任何会落到同一 `SessionPool` 的取用、同一 `ModelPool` 同键的发现。
|
|
173
|
+
|
|
174
|
+
## 模块
|
|
175
|
+
|
|
176
|
+
| 模块 | 职责 |
|
|
177
|
+
|---|---|
|
|
178
|
+
| `tcaicc.common.security` | 安全会话握手(`SessionFactory` / `SecuritySession` / `SessionPool`)+ 数据面加解密(`Request` / `StreamDecryptor` / `Policy`)+ 错误码 |
|
|
179
|
+
| `tcaicc.common.cipherx` | prompt/response/chunk 的 Envelope 密文格式(AES-256-GCM + AAD 绑定) |
|
|
180
|
+
| `tcaicc.common.cryptox` | 底层原语:AEAD、HKDF、HMAC、LP 编码、X25519 + ML-KEM-768 混合 KEX、常数时间比较 |
|
|
181
|
+
|
|
182
|
+
内置 AMD 信任锚以 PEM 文件随包分发(`tcaicc/common/security/trust_anchors/`),加载时逐字节校验 DER 的 SHA-256。
|
|
183
|
+
|
|
184
|
+
## 测试
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
uv run pytest # 全量用例(含参考服务端端到端握手;tests/ 被仓库 .gitignore 忽略,fresh clone 无此目录)
|
|
188
|
+
uv run ruff check . # lint
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
覆盖:cipherx KAT 黄金向量比对、内层填充(桶表字面值 / 桶校验 / 剥离失败)、DEK/token 派生固定向量、`Policy` 路径遍历与 JSON 结构级加解密、SEV 报告解析/链验/签名/绑定、参考服务端完整两趟握手(derive/import)、`SessionPool`(num 容量/共享取用/到期删除/重取)与 `EndpointPool`/`ModelPool`(发现/绑定/换绑/引用计数/并发单发现)、flags 词表与集群端点头快照、内置信任锚 DER SHA-256。
|
|
192
|
+
|
|
193
|
+
## 目录结构
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
aicc-sdk/python/
|
|
197
|
+
├── src/tcaicc/
|
|
198
|
+
│ ├── __init__.py # SDK 入口(常用公开 API 经此导出;Mode 等少数名字在 tcaicc.common.* 下)
|
|
199
|
+
│ └── common/
|
|
200
|
+
│ ├── security/ # 握手 + 数据面 + 会话池 + 信任锚
|
|
201
|
+
│ ├── cipherx/ # Envelope 密文格式 + KAT
|
|
202
|
+
│ └── cryptox/ # 底层密码学原语
|
|
203
|
+
├── doc/
|
|
204
|
+
│ ├── doc-python.html # 完整文档(用法说明 + 原理简介)
|
|
205
|
+
│ ├── demo.py # 非流式 demo(ModelPool 发现端点)
|
|
206
|
+
│ ├── demo_stream.py # 流式 demo(ModelPool 发现端点)
|
|
207
|
+
│ ├── demo_endpoint.py # 非流式 demo(固定端点)
|
|
208
|
+
│ └── demo_endpoint_stream.py # 流式 demo(固定端点)
|
|
209
|
+
├── tests/ # pytest 用例(未入库:被仓库根 .gitignore 忽略)
|
|
210
|
+
├── pyproject.toml
|
|
211
|
+
└── install.sh
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## License
|
|
215
|
+
|
|
216
|
+
Apache-2.0。
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# tcaicc
|
|
2
|
+
|
|
3
|
+
Tencent AICC 机密模型的 Python SDK。
|
|
4
|
+
|
|
5
|
+
接入分两步:
|
|
6
|
+
|
|
7
|
+
1. **建立安全会话** —— 与运行在 SEV-SNP TEE 中的 aicc-service 握手,取得两端一致的 32B SessionKey(含远程证明验证)。推荐用 `SessionPool` 托管会话生命周期。
|
|
8
|
+
2. **加密的 LLM 调用** —— 用该 SessionKey 加密 prompt、解密网关回文。加解密全程在本地完成,网关只见密文。
|
|
9
|
+
|
|
10
|
+
完整文档见 **[`doc/doc-python.html`](doc/doc-python.html)**(用法说明 + 原理简介);可运行示例分两套:按 model 经 `ModelPool` 发现端点的 [`doc/demo.py`](doc/demo.py)(非流式)/ [`doc/demo_stream.py`](doc/demo_stream.py)(流式),与直接指定端点的 [`doc/demo_endpoint.py`](doc/demo_endpoint.py)(非流式)/ [`doc/demo_endpoint_stream.py`](doc/demo_endpoint_stream.py)(流式,额外需 `AICC_ENDPOINT`)。
|
|
11
|
+
|
|
12
|
+
## 安装
|
|
13
|
+
|
|
14
|
+
要求 Python ≥ 3.10。
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bash install.sh # 从源码构建并安装(覆盖旧版)
|
|
18
|
+
bash install.sh --prod xxx.whl # 安装指定 wheel
|
|
19
|
+
pip install tcaicc # 或从私有源安装
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
运行时依赖 `cryptography`(≥48)、`httpx`(≥0.27);测试依赖 `pytest`、`ruff`。
|
|
23
|
+
|
|
24
|
+
## 快速开始
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
|
|
30
|
+
import httpx
|
|
31
|
+
|
|
32
|
+
from tcaicc import ModelPool, SessionFactory, SessionPool, default_policy, Protocol
|
|
33
|
+
|
|
34
|
+
base_url = os.getenv("AICC_BASE_URL") or "https://aicc.example.com/v1/aicc"
|
|
35
|
+
gateway_url = os.getenv("AICC_GATEWAY_URL") or "https://aicc.example.com/v1/chat/completions"
|
|
36
|
+
api_key = os.getenv("AICC_API_KEY")
|
|
37
|
+
model_name = os.getenv("AICC_MODEL_NAME") or "your-model"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build(endpoint): # 每端点一个独立工厂;端点由 ModelPool 统一注入
|
|
41
|
+
return SessionPool(
|
|
42
|
+
SessionFactory(base_url=base_url, ttl=60 * 60 * 8, api_key=api_key),
|
|
43
|
+
4, # num = 池容量(会话数),非并发上限
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
with ModelPool(base_url, api_key, 60 * 60 * 8, build) as models:
|
|
48
|
+
req = models.new_request(model_name) # 发现+绑定+取会话+本轮请求(每轮一个)
|
|
49
|
+
policy = default_policy(Protocol.OPENAI) # 内置默认路径,与网关互锚
|
|
50
|
+
body = {"model": model_name, "messages": [{"role": "user", "content": "你好"}]}
|
|
51
|
+
enc_body = req.encrypt_request(json.dumps(body).encode(), policy) # 按 policy 路径加密整个 body
|
|
52
|
+
|
|
53
|
+
r = httpx.post(gateway_url, content=enc_body,
|
|
54
|
+
headers={"Content-Type": "application/json",
|
|
55
|
+
"X-AICC-Mode": "1",
|
|
56
|
+
"Authorization": f"Bearer {api_key}",
|
|
57
|
+
"X-AICC-Endpoint": req.endpoint}) # 握手时的集群端点,网关据此路由
|
|
58
|
+
plain = req.decrypt_response(r.content, policy) # 按 policy 路径解密整个响应
|
|
59
|
+
print(json.loads(plain)["choices"][0]["message"]["content"])
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
> **每轮 HTTP 请求必须新建一个 `Request`**:同一请求内所有密文段须出自同一个 `Request`,否则网关因凭证不一致返回 400。
|
|
63
|
+
|
|
64
|
+
> `set_api_key` 只展开 `Authorization: Bearer`(不再带 `X-AICC-Mode`)。
|
|
65
|
+
|
|
66
|
+
`req.encrypt` / `req.decrypt` 为 raw 级(原始信封字节,base64 由调用方处理,须显式给位置);`req.encrypt_request` / `req.decrypt_response` 为 JSON 结构级(内部 base64,按 `Policy` 路径处理整个 body,位置自算)。两者示例见下节。
|
|
67
|
+
|
|
68
|
+
### 逐段加解密(raw)与整体加解密
|
|
69
|
+
|
|
70
|
+
整体(按 `Policy` 路径批量处理整个 body / 响应,位置由 SDK 自算)——即上文快速开始用法:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from tcaicc import default_policy, Protocol
|
|
74
|
+
|
|
75
|
+
policy = default_policy(Protocol.OPENAI) # 内置默认路径
|
|
76
|
+
enc_body = req.encrypt_request(json.dumps(body).encode(), policy)
|
|
77
|
+
plain = req.decrypt_response(resp_bytes, policy)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
逐段(raw,精确控制单字段)——返回/接收原始信封字节,调用方自行 base64 并置入 JSON,须显式给位置(`field_path` = 该字段的 policy 路径、`seq` = 同路径内第几段,从 0 起):
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
import base64
|
|
84
|
+
|
|
85
|
+
# 加密:密文信封 base64 后置入请求字段
|
|
86
|
+
body = {
|
|
87
|
+
"model": model_name,
|
|
88
|
+
"messages": [{"role": "user",
|
|
89
|
+
"content": base64.b64encode(
|
|
90
|
+
req.encrypt("你好".encode(), "messages.content", 0)).decode()}],
|
|
91
|
+
}
|
|
92
|
+
# 解密:从响应字段取出 base64 → 信封字节 → 明文
|
|
93
|
+
field = r.json()["choices"][0]["message"]["content"]
|
|
94
|
+
plain = req.decrypt(base64.b64decode(field), "choices.message.content", 0).decode()
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 流式响应(SSE)
|
|
98
|
+
|
|
99
|
+
流式响应由调用方自行按 SSE 切分事件,逐事件交给 `StreamDecryptor` 解密。`decrypt_event` 按 `policy` 自动定位密文段(与 `decrypt_response` 同一套路径),返回 `(事件 JSON, is_last)`;返回 `None` 表示该事件是网关合成的尾片(勿转发),零命中 / 非 JSON 事件(如 `[DONE]`)原样返回事件字节,`is_last` 收到合法尾片后恒为 `True`。**只有 Strict 有尾片**:默认 Compatible / Simple 对新 gateway 不发尾片、`is_last` 恒 `False`,以协议终止帧(`[DONE]` / `message_stop` / Responses 终止事件)+ EOF 收束,且只有逐片完整性(丢片 / 截断不可察),要整流完整性用 `Mode.STRICT`。同一 `Request` 勿并发多条流;Compatible / Simple 下流结束后 gateway 异步释放槽位,勿把同一 `Request` 立刻用于下一次 HTTP 请求。非流式 `decrypt_response` 缺尾片只在 Strict 报 `ErrStreamTruncated`(`tcaicc.common.security`)。
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
sd = req.new_stream_decryptor()
|
|
103
|
+
with httpx.stream("POST", gateway_url, content=enc_body,
|
|
104
|
+
headers={"Content-Type": "application/json",
|
|
105
|
+
"X-AICC-Mode": "1",
|
|
106
|
+
"Authorization": f"Bearer {api_key}",
|
|
107
|
+
"X-AICC-Endpoint": req.endpoint}) as r:
|
|
108
|
+
for line in r.iter_lines():
|
|
109
|
+
if not line.startswith("data:"):
|
|
110
|
+
continue
|
|
111
|
+
data = line[len("data:"):].strip()
|
|
112
|
+
if data == "[DONE]":
|
|
113
|
+
break
|
|
114
|
+
plain, is_last = sd.decrypt_event(data.encode(), policy)
|
|
115
|
+
choices = json.loads(plain).get("choices") if plain else None
|
|
116
|
+
if choices: # usage 等事件 choices 为空
|
|
117
|
+
print(choices[0].get("delta", {}).get("content") or "", end="")
|
|
118
|
+
if is_last: # 对新 gateway 仅 Strict 会翻 True
|
|
119
|
+
break
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 长度隐藏
|
|
123
|
+
|
|
124
|
+
prompt 与非流式 response 的内层按 1.5 倍几何桶填充(256 B 起,顶桶 9,695,112,共 27 桶),中间层只见所在的桶、不见精确长度;`pl` 即桶长,Hash 仍只盖真实数据。单段明文 ≤ `MAX_PADDED_PLAINTEXT_LEN`(9,695,108 B),超出抛 `TooLargeError`;`padded_len(n)` 可预估填充后长度。流式 chunk 不填充,片长仍可见;非流式回文仅在中间层逐字节透传时成立。
|
|
125
|
+
|
|
126
|
+
### 会话池 / 端点池 / 模型池
|
|
127
|
+
|
|
128
|
+
- `SessionPool(factory, num, key_generator=None)`:惰性托管至多 `num` 个共享会话;`get()` 取一个共享会话(勿 `close`),`new_request()` 一步取本轮请求(取到即被淘汰时自动重取)。`set_endpoint()` / `get_endpoint()` 转调工厂。默认 derive 模式,传 `key_generator`(返回 32B)走 import:会话经 `factory.new_session_with_key_generator(key_gen)` 建立,建会话与该会话每次 `renew()` 都重调一次(长度不符 `CryptoKeySizeError`(security 包的 `KeySizeError`,顶层别名)、回调抛错 → `KeyGeneratorError`(`"aicc sdk: key generator: <原错误>"`,原异常在 `__cause__`,无错误码),均在出网前;key 用后随机重写);`new_session(key)` 固定 key 的会话 `renew` 重导入同一把;同一会话并发 `renew` 串行化。
|
|
129
|
+
- `EndpointPool(ttl)`:`endpoint → SessionPool` 固定寿命托管(`ttl` 秒,`-1` 永不过期);`set/get/delete/close`,到期或替换即关旧池。
|
|
130
|
+
- `ModelPool(base_url, api_key, ttl, build)`:按 `model+flags` 经 `describe_endpoints` 发现集群端点、绑定期 `ttl`、引用计数、刷新失败沿用旧绑定;`build(endpoint) -> SessionPool` 须每端点新建独立工厂。
|
|
131
|
+
- 集群端点:`describe_endpoints` 返回不透明句柄(形如 `ept-…`),经 `factory.set_endpoint()` 于握手时以 `X-AICC-Endpoint` 头发出(不校验、不入 transcript);`session.get_endpoint()` / `request.endpoint` 读回该路由值。
|
|
132
|
+
- flags 词表:`FLAG_PROMPT_SHORT` / `FLAG_PROMPT_LONG`(路由提示,服务端本期不解析)。
|
|
133
|
+
- 端点发现失败(服务端端点密钥未配等)抛 `SecuritySessionError`,`code == ENDPOINT_NOT_CONFIGURED`;不影响握手与数据面。
|
|
134
|
+
- 默认位置校验模式为 **Compatible**(位置/协议不符记 info 日志后照解,默认阈值下不可见);要拒绝不符显式 `factory.set_mode(Mode.STRICT)`(`Mode` 自 `tcaicc.common.cipherx` 导入)。
|
|
135
|
+
- 握手与 `describe_endpoints` 每次尝试 15 秒超时(重试各自计时,默认传输施加);`CreateSecuritySession` 与 `describe_endpoints` 遇传输失败 / 超时 / 5xx 有限重试(最多再试 2 次,退避 200ms、400ms,同一 RequestId);业务错误与 Import / Derive 一次性消费不重试;单次响应上限 1 MiB。`set_transport` 注入的函数返回 None 或抛异常视同网络错误(文案含 `transport returned nil response` / 原异常),按上述规则重试。
|
|
136
|
+
|
|
137
|
+
### 日志
|
|
138
|
+
|
|
139
|
+
日志是**进程全局**的,没有按对象的设置项(对齐 Go `sdk.SetLogger` / `sdk.SetLogLevel`,spec `2026-10-10-sdk-logging-levels-design.md`):
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
import tcaicc
|
|
143
|
+
|
|
144
|
+
tcaicc.set_logger(my_logger) # 实现 debugf(msg) / infof(msg) / errorf(msg) 三方法即可;None 恢复默认(stderr)
|
|
145
|
+
tcaicc.set_log_level(tcaicc.LogLevel.INFO) # 阈值 DEBUG < INFO < ERROR < NONE;默认 ERROR;非法值 no-op
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- 四级:`debug` 每请求 / 每次尝试的细节(握手每个 Action 的成功与重试、发现结果、请求创建、加解密命中数、池新建决策);`info` 生命周期与被容忍 / 吞掉的事件(建会话、续约、会话入池出池、端点绑定换绑释放、Compatible / Simple 容忍的不一致、聚合分支、建会话失败回落、刷新失败沿用旧绑定、未钉 ARK);`error` 当前没有落点;`none` 不落任何日志。
|
|
149
|
+
- **默认阈值 error,而 SDK 当前没有 error 级落点,故默认零输出**;要看容忍 / 回落 / 生命周期开 info,看每请求细节开 debug。
|
|
150
|
+
- logger 收到的是已格式化的一行文本,全部以 `aicc sdk: ` 开头,不含明文、密钥、api_key、token。默认实现写 stderr,前缀 `[DEBUG] ` / `[INFO] ` / `[ERROR] `。
|
|
151
|
+
- 两个设置立即生效,对已建的工厂 / 池 / 会话 / 请求 / 流一视同仁;logger 须线程安全、勿阻塞、勿抛异常。SDK 不在自身锁内调用 logger(`renew` 期间握手层日志除外),但 logger 内勿同步等待自己可能正处于其中的 SDK 工作:同一会话的 `renew`、任何会落到同一 `SessionPool` 的取用、同一 `ModelPool` 同键的发现。
|
|
152
|
+
|
|
153
|
+
## 模块
|
|
154
|
+
|
|
155
|
+
| 模块 | 职责 |
|
|
156
|
+
|---|---|
|
|
157
|
+
| `tcaicc.common.security` | 安全会话握手(`SessionFactory` / `SecuritySession` / `SessionPool`)+ 数据面加解密(`Request` / `StreamDecryptor` / `Policy`)+ 错误码 |
|
|
158
|
+
| `tcaicc.common.cipherx` | prompt/response/chunk 的 Envelope 密文格式(AES-256-GCM + AAD 绑定) |
|
|
159
|
+
| `tcaicc.common.cryptox` | 底层原语:AEAD、HKDF、HMAC、LP 编码、X25519 + ML-KEM-768 混合 KEX、常数时间比较 |
|
|
160
|
+
|
|
161
|
+
内置 AMD 信任锚以 PEM 文件随包分发(`tcaicc/common/security/trust_anchors/`),加载时逐字节校验 DER 的 SHA-256。
|
|
162
|
+
|
|
163
|
+
## 测试
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
uv run pytest # 全量用例(含参考服务端端到端握手;tests/ 被仓库 .gitignore 忽略,fresh clone 无此目录)
|
|
167
|
+
uv run ruff check . # lint
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
覆盖:cipherx KAT 黄金向量比对、内层填充(桶表字面值 / 桶校验 / 剥离失败)、DEK/token 派生固定向量、`Policy` 路径遍历与 JSON 结构级加解密、SEV 报告解析/链验/签名/绑定、参考服务端完整两趟握手(derive/import)、`SessionPool`(num 容量/共享取用/到期删除/重取)与 `EndpointPool`/`ModelPool`(发现/绑定/换绑/引用计数/并发单发现)、flags 词表与集群端点头快照、内置信任锚 DER SHA-256。
|
|
171
|
+
|
|
172
|
+
## 目录结构
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
aicc-sdk/python/
|
|
176
|
+
├── src/tcaicc/
|
|
177
|
+
│ ├── __init__.py # SDK 入口(常用公开 API 经此导出;Mode 等少数名字在 tcaicc.common.* 下)
|
|
178
|
+
│ └── common/
|
|
179
|
+
│ ├── security/ # 握手 + 数据面 + 会话池 + 信任锚
|
|
180
|
+
│ ├── cipherx/ # Envelope 密文格式 + KAT
|
|
181
|
+
│ └── cryptox/ # 底层密码学原语
|
|
182
|
+
├── doc/
|
|
183
|
+
│ ├── doc-python.html # 完整文档(用法说明 + 原理简介)
|
|
184
|
+
│ ├── demo.py # 非流式 demo(ModelPool 发现端点)
|
|
185
|
+
│ ├── demo_stream.py # 流式 demo(ModelPool 发现端点)
|
|
186
|
+
│ ├── demo_endpoint.py # 非流式 demo(固定端点)
|
|
187
|
+
│ └── demo_endpoint_stream.py # 流式 demo(固定端点)
|
|
188
|
+
├── tests/ # pytest 用例(未入库:被仓库根 .gitignore 忽略)
|
|
189
|
+
├── pyproject.toml
|
|
190
|
+
└── install.sh
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
Apache-2.0。
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# demo.py —— 非流式示例(ModelPool 按 model 发现端点):安全会话握手 → 加密请求 → 解密回文,只打印明文。
|
|
2
|
+
#
|
|
3
|
+
# 环境变量(均必填,无内置默认):AICC_BASE_URL(安全会话入口)、AICC_GATEWAY_URL(LLM 网关)、AICC_API_KEY、AICC_MODEL_NAME
|
|
4
|
+
#
|
|
5
|
+
# python doc/demo.py
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from tcaicc import ModelPool, Protocol, SessionFactory, SessionPool, default_policy
|
|
12
|
+
|
|
13
|
+
base_url = os.getenv("AICC_BASE_URL")
|
|
14
|
+
gateway_url = os.getenv("AICC_GATEWAY_URL")
|
|
15
|
+
api_key = os.getenv("AICC_API_KEY")
|
|
16
|
+
model_name = os.getenv("AICC_MODEL_NAME")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def run():
|
|
20
|
+
# ① 模型池:按 model 发现集群端点并握手,托管会话。
|
|
21
|
+
def build(endpoint):
|
|
22
|
+
return SessionPool(SessionFactory(base_url, api_key, ttl=60 * 60 * 8), 1)
|
|
23
|
+
|
|
24
|
+
with ModelPool(base_url, api_key, 60 * 60 * 8, build) as models:
|
|
25
|
+
# ② 每轮 HTTP 请求新建一个 Request(携带本次会话的集群端点)。
|
|
26
|
+
with models.new_request(model_name) as req:
|
|
27
|
+
# ③ 加密请求体后发给网关。
|
|
28
|
+
policy = default_policy(Protocol.OPENAI)
|
|
29
|
+
body = {"model": model_name, "messages": [{"role": "user", "content": "你好"}]}
|
|
30
|
+
enc_body = req.encrypt_request(json.dumps(body).encode(), policy)
|
|
31
|
+
headers = {
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
"X-AICC-Mode": "1",
|
|
34
|
+
"Authorization": f"Bearer {api_key}",
|
|
35
|
+
"X-AICC-Endpoint": req.endpoint, # 带上握手时的集群端点,网关据此路由
|
|
36
|
+
}
|
|
37
|
+
r = httpx.post(gateway_url, content=enc_body, headers=headers, timeout=600)
|
|
38
|
+
if r.status_code != 200:
|
|
39
|
+
raise RuntimeError(f"gateway {r.status_code}: {r.text}")
|
|
40
|
+
|
|
41
|
+
# ④ 解密回文,打印明文回答。
|
|
42
|
+
plain = req.decrypt_response(r.content, policy)
|
|
43
|
+
print(json.loads(plain)["choices"][0]["message"]["content"])
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
run()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# demo_endpoint.py —— 非流式示例(固定端点):安全会话握手 → 加密请求 → 解密回文,只打印明文。
|
|
2
|
+
#
|
|
3
|
+
# 环境变量(均必填,无内置默认):AICC_BASE_URL(安全会话入口)、AICC_GATEWAY_URL(LLM 网关)、AICC_API_KEY、AICC_MODEL_NAME、
|
|
4
|
+
# AICC_ENDPOINT(集群端点;不经 DescribeEndpoints 发现,直接用于握手与数据面路由)
|
|
5
|
+
#
|
|
6
|
+
# python doc/demo_endpoint.py
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from tcaicc import Protocol, SessionFactory, SessionPool, default_policy
|
|
13
|
+
|
|
14
|
+
base_url = os.getenv("AICC_BASE_URL")
|
|
15
|
+
gateway_url = os.getenv("AICC_GATEWAY_URL")
|
|
16
|
+
api_key = os.getenv("AICC_API_KEY")
|
|
17
|
+
model_name = os.getenv("AICC_MODEL_NAME")
|
|
18
|
+
endpoint = os.getenv("AICC_ENDPOINT")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def run():
|
|
22
|
+
# ① 会话池:工厂固定集群端点,握手时带 X-AICC-Endpoint 头,托管会话。
|
|
23
|
+
factory = SessionFactory(base_url, api_key, ttl=60 * 60 * 8).set_endpoint(endpoint)
|
|
24
|
+
|
|
25
|
+
with SessionPool(factory, 1) as sessions:
|
|
26
|
+
# ② 每轮 HTTP 请求新建一个 Request(携带本次会话的集群端点)。
|
|
27
|
+
with sessions.new_request() as req:
|
|
28
|
+
# ③ 加密请求体后发给网关。
|
|
29
|
+
policy = default_policy(Protocol.OPENAI)
|
|
30
|
+
body = {"model": model_name, "messages": [{"role": "user", "content": "你好"}]}
|
|
31
|
+
enc_body = req.encrypt_request(json.dumps(body).encode(), policy)
|
|
32
|
+
headers = {
|
|
33
|
+
"Content-Type": "application/json",
|
|
34
|
+
"X-AICC-Mode": "1",
|
|
35
|
+
"Authorization": f"Bearer {api_key}",
|
|
36
|
+
"X-AICC-Endpoint": req.endpoint, # 带上握手时的集群端点,网关据此路由
|
|
37
|
+
}
|
|
38
|
+
r = httpx.post(gateway_url, content=enc_body, headers=headers, timeout=600)
|
|
39
|
+
if r.status_code != 200:
|
|
40
|
+
raise RuntimeError(f"gateway {r.status_code}: {r.text}")
|
|
41
|
+
|
|
42
|
+
# ④ 解密回文,打印明文回答。
|
|
43
|
+
plain = req.decrypt_response(r.content, policy)
|
|
44
|
+
print(json.loads(plain)["choices"][0]["message"]["content"])
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
run()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# demo_endpoint_stream.py —— 流式示例(固定端点):安全会话握手 → 加密请求 → 逐 SSE 事件解密,只打印明文。
|
|
2
|
+
#
|
|
3
|
+
# 环境变量(均必填,无内置默认):AICC_BASE_URL(安全会话入口)、AICC_GATEWAY_URL(LLM 网关)、AICC_API_KEY、AICC_MODEL_NAME、
|
|
4
|
+
# AICC_ENDPOINT(集群端点;不经 DescribeEndpoints 发现,直接用于握手与数据面路由)
|
|
5
|
+
#
|
|
6
|
+
# python doc/demo_endpoint_stream.py
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from tcaicc import Protocol, SessionFactory, SessionPool, default_policy
|
|
13
|
+
|
|
14
|
+
base_url = os.getenv("AICC_BASE_URL")
|
|
15
|
+
gateway_url = os.getenv("AICC_GATEWAY_URL")
|
|
16
|
+
api_key = os.getenv("AICC_API_KEY")
|
|
17
|
+
model_name = os.getenv("AICC_MODEL_NAME")
|
|
18
|
+
endpoint = os.getenv("AICC_ENDPOINT")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def run():
|
|
22
|
+
# ① 会话池:工厂固定集群端点,握手时带 X-AICC-Endpoint 头,托管会话。
|
|
23
|
+
factory = SessionFactory(base_url, api_key, ttl=60 * 60 * 8).set_endpoint(endpoint)
|
|
24
|
+
|
|
25
|
+
with SessionPool(factory, 1) as sessions:
|
|
26
|
+
# ② 每轮 HTTP 请求新建一个 Request(携带本次会话的集群端点)。
|
|
27
|
+
with sessions.new_request() as req:
|
|
28
|
+
# ③ 加密请求体(stream=true)后发给网关。
|
|
29
|
+
policy = default_policy(Protocol.OPENAI)
|
|
30
|
+
body = {"model": model_name, "messages": [{"role": "user", "content": "你好"}], "stream": True}
|
|
31
|
+
enc_body = req.encrypt_request(json.dumps(body).encode(), policy)
|
|
32
|
+
headers = {
|
|
33
|
+
"Content-Type": "application/json",
|
|
34
|
+
"Accept": "text/event-stream",
|
|
35
|
+
"X-AICC-Mode": "1",
|
|
36
|
+
"Authorization": f"Bearer {api_key}",
|
|
37
|
+
"X-AICC-Endpoint": req.endpoint, # 带上握手时的集群端点,网关据此路由
|
|
38
|
+
}
|
|
39
|
+
timeout = httpx.Timeout(600, read=None)
|
|
40
|
+
with httpx.stream("POST", gateway_url, content=enc_body, headers=headers, timeout=timeout) as r:
|
|
41
|
+
if r.status_code != 200:
|
|
42
|
+
raise RuntimeError(f"gateway {r.status_code}: {r.read().decode()}")
|
|
43
|
+
|
|
44
|
+
# ④ 逐个 SSE 事件解密,打印增量明文。
|
|
45
|
+
sd = req.new_stream_decryptor()
|
|
46
|
+
for line in r.iter_lines():
|
|
47
|
+
if not line.startswith("data: "):
|
|
48
|
+
continue
|
|
49
|
+
data = line[len("data: "):]
|
|
50
|
+
if data == "[DONE]":
|
|
51
|
+
break
|
|
52
|
+
plain, is_last = sd.decrypt_event(data.encode(), policy)
|
|
53
|
+
choices = json.loads(plain).get("choices") if plain else None
|
|
54
|
+
if choices:
|
|
55
|
+
print(choices[0].get("delta", {}).get("content") or "", end="", flush=True)
|
|
56
|
+
if is_last:
|
|
57
|
+
break
|
|
58
|
+
print()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
run()
|