From 8d963439785d9651a3b4657d3b8ff18ca206f71b Mon Sep 17 00:00:00 2001 From: "hanzhi.421" Date: Tue, 13 Jan 2026 12:14:13 +0800 Subject: [PATCH 1/2] fix: adjustment of expiration time and handling after response_id expires --- veadk/models/ark_llm.py | 63 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/veadk/models/ark_llm.py b/veadk/models/ark_llm.py index 0da72735..c1eba85a 100644 --- a/veadk/models/ark_llm.py +++ b/veadk/models/ark_llm.py @@ -16,6 +16,7 @@ import base64 import json +import time from typing import Any, Dict, Union, AsyncGenerator, Tuple, List, Optional, Literal from typing_extensions import override @@ -50,6 +51,7 @@ EasyInputMessageParam, FunctionCallOutput, ) +from volcenginesdkarkruntime._exceptions import ArkBadRequestError from veadk.config import settings from veadk.consts import DEFAULT_VIDEO_MODEL_API_BASE @@ -441,10 +443,12 @@ def get_model_without_provider(request_data: dict) -> dict: def filtered_inputs( - inputs: List[ResponseInputItemParam], + inputs: List[ResponseInputItemParam], previous_response_id: Optional[str] = None ) -> List[ResponseInputItemParam]: # Keep the first message and all consecutive user messages from the end # Collect all consecutive user messages from the end + if previous_response_id is None: + return inputs new_inputs = [] for m in reversed(inputs): # Skip the first message if m.get("type") == "function_call_output" or m.get("role") == "user": @@ -477,13 +481,23 @@ def request_reorganization_by_ark(request_data: Dict) -> Dict: request_data = get_model_without_provider(request_data) # 2. filtered input - request_data["input"] = filtered_inputs(request_data["input"]) + request_data["input"] = filtered_inputs( + request_data.get("input"), + previous_response_id=request_data.get("previous_response_id", None), + ) # 3. filter not support data request_data = { key: value for key, value in request_data.items() if key in ark_supported_fields } + # expire time 1 hour when send aresponses req + extra_body = request_data.get("extra_body") + if not isinstance(extra_body, dict): + extra_body = {} + request_data["extra_body"] = extra_body + extra_body["expire_at"] = int(time.time()) + 3600 + # [Note: Ark Limitations] caching and text # After enabling caching, output_schema(text) cannot be used. Caching must be disabled. if _is_caching_enabled(request_data) and request_data.get("text") is not None: @@ -625,7 +639,7 @@ def ark_response_to_generate_content_response( class ArkLlmClient: - async def aresponse( + async def aresponses( self, **kwargs ) -> Union[ArkTypeResponse, AsyncStream[ResponseStreamEvent]]: # 1. Get request params @@ -706,18 +720,55 @@ async def generate_content_async( if generation_params: responses_args.update(generation_params) + try: + async for llm_response in self.generate_content_via_responses( + responses_args.copy(), stream=stream + ): + yield llm_response + except ArkBadRequestError as e: + # Check if it is PreviousResponseNotFound + is_expired = False + if hasattr(e, "body") and isinstance(e.body, dict): + if e.body.get("code") == "InvalidParameter.PreviousResponseNotFound": + is_expired = True + + if is_expired: + logger.warning( + f"Interaction expired (PreviousResponseNotFound). Retrying without previous_response_id. Error: {e}" + ) + # Remove previous_response_id + if "previous_response_id" in responses_args: + del responses_args["previous_response_id"] + # Retry + try: + async for llm_response in self.generate_content_via_responses( + responses_args.copy(), stream=stream + ): + yield llm_response + except Exception as retry_e: + logger.error(f"Retry failed in generate_content_async: {retry_e}") + raise retry_e + else: + logger.error(f"Error in generate_content_async: {e}") + raise e + except Exception as e: + logger.error(f"Error in generate_content_async: {e}") + raise e + + async def generate_content_via_responses( + self, responses_args: dict, stream: bool = False + ): responses_args = request_reorganization_by_ark(responses_args) - if stream: responses_args["stream"] = True - async for part in await self.llm_client.aresponse(**responses_args): + async for part in await self.llm_client.aresponses(**responses_args): llm_response = event_to_generate_content_response( event=part, is_partial=True, model_version=self.model ) if llm_response: yield llm_response else: - raw_response = await self.llm_client.aresponse(**responses_args) + raw_response = await self.llm_client.aresponses(**responses_args) llm_response = ark_response_to_generate_content_response(raw_response) yield llm_response From c3b2315e616a6427287d2b81c32e0c733a3cb41f Mon Sep 17 00:00:00 2001 From: "hanzhi.421" Date: Tue, 13 Jan 2026 14:18:51 +0800 Subject: [PATCH 2/2] fix: responses api document --- docs/docs/agent/responses-api.md | 114 ++++++++++++++++--------------- 1 file changed, 58 insertions(+), 56 deletions(-) diff --git a/docs/docs/agent/responses-api.md b/docs/docs/agent/responses-api.md index 61996aa2..f79ce6ee 100644 --- a/docs/docs/agent/responses-api.md +++ b/docs/docs/agent/responses-api.md @@ -22,6 +22,8 @@ Responses API 是火山方舟最新推出的 API 接口,原生支持高效的 ### 快速开始 +**开启 Responses API** + 只需配置 `enable_responses=True` 即可。 ```python hl_lines="4" @@ -33,61 +35,12 @@ root_agent = Agent( ``` -### 效果展示 +**效果展示** ![responses-api](../assets/images/agents/responses_api.png) -## 注意事项 - -1. **版本要求**:必须保证 `google-adk>=1.21.0`。 -2. **模型支持**:请确保使用的模型支持 Responses API(注:Doubao 系列模型 0615 版本之后,除特殊说明外均支持)。 -3. **缓存机制**:VeADK 开启 Responses API 并使用火山方舟模型时默认开启上下文缓存(Session 缓存)。但若在 Agent 中设置了 `output_schema`,因该字段与缓存机制冲突,系统将自动关闭缓存。 - -## 上下文缓存 - -在 Responses API 模式下,VeADK 默认开启会话缓存(Session Caching)。该机制会自动存储初始上下文信息,并在每一轮对话中动态更新。在后续请求中,系统会将缓存内容与新输入合并后发送给模型推理。此功能特别适用于多轮对话、复杂工具调用等长上下文场景。 - -### 缓存信息查看 - -您可以通过返回的 `Event.usage_metadata` 字段查看 Token 使用及缓存命中情况。 - -以下是一次包含两轮对话的 `usage_metadata` 示例: - -```json -{"cached_content_token_count":0,"candidates_token_count":87,"prompt_token_count":210,"total_token_count":297} -{"cached_content_token_count":297,"candidates_token_count":181,"prompt_token_count":314,"total_token_count":495} -``` - -**字段说明:** - -- `cached_content_token_count`:命中缓存的 Token 数量(即从缓存中读取的 Token 数)。 -- `candidates_token_count`:模型生成的 Token 数量(输出 Token)。 -- `prompt_token_count`:输入给模型的总 Token 数量(包含已缓存和未缓存部分)。 -- `total_token_count`:总消耗 Token 数量(输入 + 输出)。 - -**缓存机制说明:** - -- 缓存仅影响输入(Prompt)Token,不影响输出(Completion)Token。 -- **缓存命中率**反映了缓存策略的有效性,命中率越高,Token 成本节省越多。 - - 计算公式:`缓存命中率 = (cached_content_token_count / prompt_token_count) × 100%` -- 输入 Token 成本节约率:用于量化整个会话的缓存收益,是面向业务侧的核心指标,支持会话级汇总计算。 - -### 成本节省示例 - -基于上述样例数据,缓存命中率计算如下: - -- **第一轮对话**:0%(初始状态,无缓存) -- **第二轮对话**:`297 / 314 * 100% ≈ 94.58%` - -输入 Token 成本节约率:`(0 + 297) / (210 + 314) ≈ 56.68%` - -这意味着在开启缓存后,该次会话的 **输入 Token 缓存命中率达到了 56.68%**,大幅减少了重复内容的计算开销。 -[火山方舟:缓存Token计费说明](https://www.volcengine.com/docs/82379/1544106?lang=zh) - -注:第N轮的`cached_content_token_count`不一定等于第N-1轮的`total_token_count`,如果开启了thinking,二者不等。 - -## 多模态能力支持 +### 多模态能力支持 Responses API 除文本交互外,还具备图片、视频和文件等多模态理解能力。 您可以使用 `google.genai.types.FileData` 字段传递多模态数据(如图片路径、视频 URL、Files API 生成的 file_id 等)。 @@ -110,7 +63,7 @@ Responses API 除文本交互外,还具备图片、视频和文件等多模态 - 支持传入 Base64 编码的数据。 - 参考:[火山方舟:图片理解-Base64编码](https://www.volcengine.com/docs/82379/1362931?lang=zh#477e51ce) -### 样例代码 +#### 样例代码 **注**:以下所有示例代码均基于下述 `Agent` 配置与 `main` 函数: @@ -155,7 +108,7 @@ async def main(message: types.Content): ``` -### 图片理解 +#### 图片理解 === "本地路径" @@ -229,7 +182,7 @@ async def main(message: types.Content): -### 视频理解 +#### 视频理解 === "Files API" @@ -289,7 +242,7 @@ async def main(message: types.Content): ``` -### 文档理解 +#### 文档理解 部分模型支持处理 PDF 格式的文档,系统会通过视觉功能理解整个文档的上下文。 当传入 PDF 文档时,大模型会将文件分页处理成多张图片,分析解读其中的文本与图片信息,并结合这些信息完成文档理解任务。 @@ -339,6 +292,55 @@ async def main(message: types.Content): ) ``` +## 上下文缓存 + +在 Responses API 模式下,VeADK 默认开启会话缓存(Session Caching)。该机制会自动存储初始上下文信息,并在每一轮对话中动态更新。在后续请求中,系统会将缓存内容与新输入合并后发送给模型推理。此功能特别适用于多轮对话、复杂工具调用等长上下文场景。 + +### 缓存信息查看 + +您可以通过返回的 `Event.usage_metadata` 字段查看 Token 使用及缓存命中情况。 + +以下是一次包含两轮对话的 `usage_metadata` 示例: + +```json +{"cached_content_token_count":0,"candidates_token_count":87,"prompt_token_count":210,"total_token_count":297} +{"cached_content_token_count":297,"candidates_token_count":181,"prompt_token_count":314,"total_token_count":495} +``` + +**字段说明:** + +- `cached_content_token_count`:命中缓存的 Token 数量(即从缓存中读取的 Token 数)。 +- `candidates_token_count`:模型生成的 Token 数量(输出 Token)。 +- `prompt_token_count`:输入给模型的总 Token 数量(包含已缓存和未缓存部分)。 +- `total_token_count`:总消耗 Token 数量(输入 + 输出)。 + +**缓存机制说明:** + +- 缓存仅影响输入(Prompt)Token,不影响输出(Completion)Token。 +- **缓存命中率**反映了缓存策略的有效性,命中率越高,Token 成本节省越多。 + - 计算公式:`缓存命中率 = (cached_content_token_count / prompt_token_count) × 100%` +- 输入 Token 成本节约率:用于量化整个会话的缓存收益,是面向业务侧的核心指标,支持会话级汇总计算。 + +### 成本节省示例 + +基于上述样例数据,缓存命中率计算如下: + +- **第一轮对话**:0%(初始状态,无缓存) +- **第二轮对话**:`297 / 314 * 100% ≈ 94.58%` + +输入 Token 成本节约率:`(0 + 297) / (210 + 314) ≈ 56.68%` + +这意味着在开启缓存后,该次会话的 **输入 Token 缓存命中率达到了 56.68%**,大幅减少了重复内容的计算开销。 +[火山方舟:缓存Token计费说明](https://www.volcengine.com/docs/82379/1544106?lang=zh) + +注:第N轮的`cached_content_token_count`不一定等于第N-1轮的`total_token_count`,如果开启了thinking,二者不等。 + + +## 注意事项 + +1. **版本要求**:必须保证 `google-adk>=1.21.0`。 +2. **模型支持**:请确保使用的模型支持 Responses API(注:Doubao 系列模型 0615 版本之后,除特殊说明外均支持)。 +3. **缓存机制**:VeADK 开启 Responses API 并使用火山方舟模型时默认开启上下文缓存(Session 缓存)。但若在 Agent 中设置了 `output_schema`,因该字段与缓存机制冲突,系统将自动关闭缓存。 ## 参考文档 @@ -346,4 +348,4 @@ async def main(message: types.Content): 1. [火山方舟:ResponsesAPI迁移文档](https://www.volcengine.com/docs/82379/1585128?lang=zh) 2. [火山方舟:上下文缓存](https://www.volcengine.com/docs/82379/1602228?lang=zh#3e69e743) 3. [火山方舟:缓存Token计费说明](https://www.volcengine.com/docs/82379/1544106?lang=zh) -4. [火山方舟:多模态理解](https://www.volcengine.com/docs/82379/1958521?lang=zh) \ No newline at end of file +4. [火山方舟:多模态理解](https://www.volcengine.com/docs/82379/1958521?lang=zh)