zai-rs 0.6.0

Type-safe async Rust SDK for Zhipu AI (BigModel) APIs
Documentation
# 最佳实践

以下约定适用于长期运行、并发调用 API 的服务端应用。

## 1. API 密钥管理

### 使用环境变量
```rust,ignore
// 从 ZHIPU_API_KEY 构造共享客户端。
let client = ZaiClient::from_env()?;

// 不要在源码中硬编码真实密钥。
let client = ZaiClient::builder("example-id.example-secret-value").build()?;
```

### 使用 `.env` 文件
```dotenv
ZHIPU_API_KEY=example-id.example-secret-value
```

`.env` 不是 SDK 自动加载功能;如需使用,请由应用选择并配置相应的 dotenv
库,然后仍通过 `ZaiClient::from_env()` 创建客户端。

### 验证 API 密钥格式
```rust,ignore
use zai_rs::client::error::validate_api_key;

validate_api_key(&api_key)?;
let client = ZaiClient::builder(api_key).build()?;
```

## 2. 错误处理

### 使用 `Result` 类型
```rust,ignore
async fn chat_with_client(client: &ZaiClient, prompt: &str) -> ZaiResult<String> {
    let response = ChatCompletion::new(GLM4_5_flash {}, TextMessage::user(prompt))
        .send_via(client)
        .await?;
    Ok(response
        .choices()
        .and_then(|choices| choices.first())
        .and_then(|choice| choice.message())
        .and_then(|message| message.content_str())
        .unwrap_or_default()
        .to_owned())
}

// 对比示例:不要在生产请求路径中使用 unwrap。
async fn chat_bad(client: &ZaiClient, prompt: &str) -> String {
    let response = ChatCompletion::new(GLM4_5_flash {}, TextMessage::user(prompt))
        .send_via(client)
        .await
        .unwrap();
    response.choices().unwrap()[0]
        .message()
        .unwrap()
        .content_str()
        .unwrap()
        .to_owned()
}
```

### 区分错误类型
```rust,ignore
match result {
    Ok(response) => { /* 处理成功 */ },
    Err(ZaiError::AuthError { .. }) => {
        // 提示用户检查 API 密钥
    },
    Err(ZaiError::RateLimitError { .. }) => {
        // 提示用户稍后重试
    },
    Err(ZaiError::NetworkError(_)) => {
        // 网络错误,可能需要检查连接
    },
    Err(e) => {
        // 其他错误
        tracing::error!("Unexpected error: {}", e);
    },
}
```

### 使用重试机制

`max_attempts` 包含首次请求。自动重试只适用于幂等 HTTP 方法;普通
`POST/PATCH` 不会被隐式重放。

```rust,ignore
use zai_rs::client::{HttpTransportConfig, ZaiClient};
use std::time::Duration;

let config = HttpTransportConfig::builder()
    .max_attempts(3)?
    .request_timeout(Duration::from_secs(60))?
    .build();
let client = ZaiClient::builder(api_key).transport(config).build()?;
```

## 3. 请求优化

### 启用压缩
统一传输层默认启用 gzip,并由 `ZaiClient` 持有的单一连接池透明解压响应;请求对象不会各自创建客户端。

### 设置合适的超时
```rust,ignore
let config = HttpTransportConfig::builder()
    .request_timeout(Duration::from_secs(30))?
    .build();
```

### 使用类型安全的流式响应
```rust,ignore
let mut stream = ChatCompletion::new(GLM4_5_flash {}, TextMessage::user(prompt))
    .with_max_tokens(1_000)
    .enable_stream()
    .stream_via(&client)
    .await?;

while let Some(chunk) = stream.next().await {
    let chunk = chunk?;
    if let Some(text) = chunk
        .choices
        .first()
        .and_then(|choice| choice.delta.as_ref())
        .and_then(|delta| delta.content.as_deref())
    {
        print!("{text}");
    }
}
```

旧版文档中的 `chat_completions_stream`、`sse_stream` 和
`stream_sse_for_each` 已删除;当前入口统一为 `stream_via(&client)`。

## 4. 日志和监控

### 配置日志
```rust,ignore
use tracing_subscriber;

tracing_subscriber::fmt()
    .with_max_level(tracing::Level::INFO)
    .init();

// ZaiClient 的 Debug 输出会隐藏密钥;应用自己的日志仍需自行脱敏。
```

### 结构化日志
```rust,ignore
use tracing::{info, error, instrument};

#[instrument(skip(client))]
async fn process_request(client: &ZaiClient, user_id: &str) -> ZaiResult<()> {
    info!(user_id, "Processing request");
    let request = ChatCompletion::new(
        GLM4_5_flash {},
        TextMessage::user("health summary"),
    )
    .with_user_id(user_id);

    match request.send_via(client).await {
        Ok(response) => {
            info!(
                tokens_used = response
                    .usage()
                    .and_then(|usage| usage.total_tokens())
                    .unwrap_or_default(),
                "Request completed"
            );
            Ok(())
        },
        Err(e) => {
            error!(error = %e, user_id, "Request failed");
            Err(e)
        },
    }
}
```

### 过滤敏感信息
```rust,ignore
use zai_rs::client::error::mask_sensitive_info;

// 仅在必须记录外部文本时兜底清理;不要主动把 key 拼进日志。
let safe_msg = mask_sensitive_info(external_message);
```

## 5. 并发和性能

### 使用连接池
```rust,ignore
// 克隆不会复制密钥内容或创建新的连接池。
let client2 = client.clone();
```

### 并发请求
```rust,ignore
use tokio::task::JoinSet;

let mut tasks = JoinSet::new();
for prompt in prompts {
    let client = client.clone();
    tasks.spawn(async move { chat_with_client(&client, &prompt).await });
}

while let Some(result) = tasks.join_next().await {
    match result? {
        Ok(content) => println!("{content}"),
        Err(error) => tracing::error!(%error, "chat failed"),
    }
}
```

### 限制并发数量
```rust,ignore
use tokio::sync::Semaphore;
use std::sync::Arc;

let semaphore = Arc::new(Semaphore::new(5));
let mut tasks = JoinSet::new();

for prompt in prompts {
    let client = client.clone();
    let semaphore = semaphore.clone();
    tasks.spawn(async move {
        let _permit = semaphore.acquire_owned().await.map_err(|_| ZaiError::ApiError {
            code: zai_rs::client::error::codes::SDK_VALIDATION,
            message: "concurrency limiter closed".to_owned(),
        })?;
        chat_with_client(&client, &prompt).await
    });
}

while let Some(result) = tasks.join_next().await {
    let _response = result??;
}
```

## 6. 内存和资源管理

### 控制模型响应规模
```rust,ignore
let request = ChatCompletion::new(GLM4_5_flash {}, TextMessage::user(prompt))
    .with_max_tokens(1_000);
let response = request.send_via(&client).await?;
process_response(&response);
```

## 7. 测试

使用本地 HTTP mock server 构造真实 `ZaiClient` 端点,断言请求方法、路径、请求体
和错误映射。仓库内可参考 `tests/support/http_server.rs` 与
`tests/send_via_coverage.rs`。

## 8. 安全性

### 使用 HTTPS
SDK 默认使用 HTTPS,无需额外配置。

### 验证输入

请求 builder 会校验冻结 API 契约中的长度、枚举和组合约束。应用仍应在构造请求前
执行自己的授权、内容策略和资源配额检查;不要编造与服务端契约无关的统一长度上限。

### 使用安全的日志级别

生产环境通常使用 `INFO`;无论日志级别如何,都不要记录原始密钥、Authorization
header 或未脱敏的用户内容。

## 9. 代码组织

### 模块化代码
```rust,ignore
// src/chat.rs
pub mod chat {
    use zai_rs::{ZaiClient, ZaiResult, model::*};

    pub async fn ask(client: &ZaiClient, prompt: &str) -> ZaiResult<String> {
        let response = ChatCompletion::new(GLM4_5_flash {}, TextMessage::user(prompt))
            .send_via(client)
            .await?;
        Ok(response
            .choices()
            .and_then(|choices| choices.first())
            .and_then(|choice| choice.message())
            .and_then(|message| message.content_str())
            .unwrap_or_default()
            .to_owned())
    }
}

// 使用
use crate::chat::chat;

let answer = chat::ask(&client, "Hello").await?;
```

### 创建辅助函数
```rust,ignore
// 封装常用配置
fn create_client() -> Result<ZaiClient, Box<dyn std::error::Error>> {
    let api_key = std::env::var("ZHIPU_API_KEY")?;
    validate_api_key(&api_key)?;

    let config = HttpTransportConfig::builder()
        .max_attempts(3)?
        .request_timeout(Duration::from_secs(60))?
        .build();

    Ok(ZaiClient::builder(api_key).transport(config).build()?)
}
```

## 10. 生产环境配置

### 环境变量管理
```bash
# .env.production
ZHIPU_API_KEY=prod-id.example-production-secret
RUST_LOG=info

# .env.development
ZHIPU_API_KEY=dev-id.example-development-secret
RUST_LOG=debug
```

### 监控和告警
```rust,ignore
// 记录关键指标
use tracing::{Instrument, info, info_span};

let request = ChatCompletion::new(
    GLM4_5_flash {},
    TextMessage::user(prompt),
);

let span = info_span!("api_call", model = "glm-4.5-flash", prompt_len = prompt.len());

async {
    let result = request.send_via(&client).await;
    if let Ok(resp) = &result {
        info!(
            tokens_used = resp
                .usage()
                .and_then(|usage| usage.total_tokens())
                .unwrap_or_default(),
            "API call completed"
        );
    }
    result
}
.instrument(span)
.await
```

### 健康检查

进程存活探针不应调用付费模型 API。就绪探针可检查本地配置是否已加载;若业务确实
需要端到端探测,应使用低频、独立配额并明确计费和限流影响。