chore: lint fix and test coverage (#19)

Co-authored-by: zhengweijun <weijun.zheng@aminer.cn>
This commit is contained in:
wellenzheng 2025-07-27 14:03:32 +08:00 committed by GitHub
parent f6ec3f2189
commit e8a26d53e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 319 additions and 267 deletions

View file

@ -31,6 +31,7 @@ from .core import (
_jwt_token,
)
class BaseClient(HttpClient):
"""
Main client for interacting with the ZAI API
@ -92,6 +93,7 @@ class BaseClient(HttpClient):
self.base_url = base_url
from ._version import __version__
super().__init__(
version=__version__,
base_url=base_url,
@ -104,7 +106,7 @@ class BaseClient(HttpClient):
@property
def default_base_url(self):
raise NotImplementedError("Subclasses must define default_base_url")
raise NotImplementedError('Subclasses must define default_base_url')
@cached_property
def chat(self) -> Chat:
@ -204,11 +206,13 @@ class BaseClient(HttpClient):
self.close()
class ZaiClient(BaseClient):
@property
def default_base_url(self):
return 'https://api.z.ai/api/paas/v4'
class ZhipuAiClient(BaseClient):
@property
def default_base_url(self):

View file

@ -34,6 +34,7 @@ class Audio(BaseAPI):
Attributes:
transcriptions (Transcriptions): Audio transcription operations
"""
@cached_property
def transcriptions(self) -> Transcriptions:
return Transcriptions(self._client)

View file

@ -30,11 +30,11 @@ if TYPE_CHECKING:
from zai._client import ZaiClient
class Transcriptions(BaseAPI):
"""
API resource for audio transcription operations
"""
def __init__(self, client: 'ZaiClient') -> None:
super().__init__(client)

View file

@ -1,3 +1,3 @@
from .batches import Batches
__all__ = ["Batches"]
__all__ = ['Batches']

View file

@ -32,6 +32,7 @@ class AsyncCompletions(BaseAPI):
Provides access to asynchronous chat completion operations.
"""
def __init__(self, client: 'ZaiClient') -> None:
super().__init__(client)
@ -125,8 +126,8 @@ class AsyncCompletions(BaseAPI):
'tool_choice': tool_choice,
'meta': meta,
'extra': maybe_transform(extra, code_geex_params.CodeGeexExtra),
"response_format": response_format,
"thinking": thinking
'response_format': response_format,
'thinking': thinking,
}
return self._post(
'/async/chat/completions',

View file

@ -15,6 +15,7 @@ class Chat(BaseAPI):
Provides access to chat completions and async completions.
"""
@cached_property
def completions(self) -> Completions:
return Completions(self._client)

View file

@ -36,6 +36,7 @@ class Completions(BaseAPI):
Attributes:
client (ZaiClient): The ZAI client instance
"""
def __init__(self, client: 'ZaiClient') -> None:
super().__init__(client)
@ -133,7 +134,7 @@ class Completions(BaseAPI):
'meta': meta,
'extra': maybe_transform(extra, code_geex_params.CodeGeexExtra),
'response_format': response_format,
"thinking": thinking
'thinking': thinking,
}
)
return self._post(

View file

@ -18,6 +18,7 @@ class Embeddings(BaseAPI):
Attributes:
client (ZaiClient): The ZAI client instance
"""
def __init__(self, client: 'ZaiClient') -> None:
super().__init__(client)

View file

@ -16,6 +16,7 @@ class Images(BaseAPI):
"""
API resource for image generation operations
"""
def __init__(self, client: 'ZaiClient') -> None:
super().__init__(client)

View file

@ -16,6 +16,7 @@ class Moderations(BaseAPI):
"""
API resource for content moderation operations
"""
def __init__(self, client: ZaiClient) -> None:
super().__init__(client)

View file

@ -31,6 +31,7 @@ class Tools(BaseAPI):
Provides access to various tool operations including web search.
"""
def __init__(self, client: 'ZaiClient') -> None:
super().__init__(client)

View file

@ -1,6 +1,6 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, List
from typing import TYPE_CHECKING, List, Optional
import httpx

View file

@ -16,6 +16,7 @@ class BaseAPI:
Attributes:
_client (ZaiClient): The client instance for making API requests
"""
_client: ZaiClient
def __init__(self, client: ZaiClient) -> None:

View file

@ -83,9 +83,7 @@ class APIResponseValidationError(APIResponseError):
class APIConnectionError(APIResponseError):
def __init__(
self, *, message: str = 'Connection error.', request: httpx.Request
) -> None:
def __init__(self, *, message: str = 'Connection error.', request: httpx.Request) -> None:
super().__init__(message, request, json_data=None)

View file

@ -20,20 +20,13 @@ from ._utils import is_mapping_t, is_sequence_t, is_tuple_t
def is_file_content(obj: object) -> TypeGuard[FileContent]:
return (
isinstance(obj, bytes)
or isinstance(obj, tuple)
or isinstance(obj, io.IOBase)
or isinstance(obj, os.PathLike)
isinstance(obj, bytes) or isinstance(obj, tuple) or isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike)
)
def assert_is_file_content(obj: object, *, key: str | None = None) -> None:
if not is_file_content(obj):
prefix = (
f'Expected entry at `{key}`'
if key is not None
else f'Expected file input `{obj!r}`'
)
prefix = f'Expected entry at `{key}`' if key is not None else f'Expected file input `{obj!r}`'
raise RuntimeError(
f'{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead. See https://github.com/openai/openai-python/tree/main#file-uploads'
) from None
@ -56,9 +49,7 @@ def to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None:
elif is_sequence_t(files):
files = [(key, _transform_file(file)) for key, file in files]
else:
raise TypeError(
f'Unexpected file type input {type(files)}, expected mapping or sequence'
)
raise TypeError(f'Unexpected file type input {type(files)}, expected mapping or sequence')
return files
@ -74,9 +65,7 @@ def _transform_file(file: FileTypes) -> HttpxFileTypes:
if is_tuple_t(file):
return (file[0], _read_file_content(file[1]), *file[2:])
raise TypeError(
'Expected file types input to be a FileContent type or to be a tuple'
)
raise TypeError('Expected file types input to be a FileContent type or to be a tuple')
def _read_file_content(file: FileContent) -> HttpxFileContent:

View file

@ -16,7 +16,7 @@ def generate_token(apikey: str):
try:
api_key, secret = apikey.split('.')
except Exception as e:
raise Exception('invalid api_key', e)
raise Exception('Invalid API key', e)
payload = {
'api_key': api_key,

View file

@ -11,6 +11,7 @@ class LoggerNameFilter(logging.Filter):
Currently allows all log records to pass through.
"""
def filter(self, record):
"""
Determine if the specified record is to be logged.
@ -38,15 +39,9 @@ def get_log_file(log_path: str, sub_dir: str):
return os.path.join(log_dir, 'zai.log')
def get_config_dict(
log_level: str, log_file_path: str, log_backup_count: int, log_max_bytes: int
) -> dict:
def get_config_dict(log_level: str, log_file_path: str, log_backup_count: int, log_max_bytes: int) -> dict:
# for windows, the path should be a raw string.
log_file_path = (
log_file_path.encode('unicode-escape').decode()
if os.name == 'nt'
else log_file_path
)
log_file_path = log_file_path.encode('unicode-escape').decode() if os.name == 'nt' else log_file_path
log_level = log_level.upper()
config_dict = {
'version': 1,

View file

@ -11,6 +11,7 @@ class AgentsChoiceDelta(BaseModel):
content (Optional[object]): The content delta
role (Optional[str]): The role of the message sender
"""
content: Optional[object] = None
role: Optional[str] = None
@ -24,6 +25,7 @@ class AgentsChoice(BaseModel):
finish_reason (Optional[str]): Reason why the generation finished
index (int): Index of this choice in the response
"""
delta: AgentsChoiceDelta
finish_reason: Optional[str] = None
index: int
@ -38,6 +40,7 @@ class AgentsCompletionUsage(BaseModel):
completion_tokens (int): Number of tokens in the completion
total_tokens (int): Total number of tokens used
"""
prompt_tokens: int
completion_tokens: int
total_tokens: int
@ -51,6 +54,7 @@ class AgentsError(BaseModel):
code (Optional[str]): Error code
message (Optional[str]): Error message
"""
code: Optional[str] = None
message: Optional[str] = None
@ -67,6 +71,7 @@ class AgentsCompletionChunk(BaseModel):
usage (Optional[AgentsCompletionUsage]): Token usage statistics
error (Optional[AgentsError]): Error information if any
"""
agent_id: Optional[str] = None
conversation_id: Optional[str] = None
id: Optional[str] = None

View file

@ -12,6 +12,7 @@ class Usage(BaseModel):
completion_tokens (int): Number of tokens in model output
total_tokens (int): Total number of tokens used
"""
prompt_tokens: int
completion_tokens: int
total_tokens: int
@ -28,6 +29,7 @@ class ConversationUsage(BaseModel):
update_time (int): Last update timestamp
usage (Usage): Token usage statistics for this conversation
"""
id: str
assistant_id: str
create_time: int
@ -44,6 +46,7 @@ class ConversationUsageList(BaseModel):
has_more (bool): Whether there are more pages available
conversation_list (List[ConversationUsage]): List of conversation usage records
"""
assistant_id: str
has_more: bool
conversation_list: List[ConversationUsage]
@ -58,6 +61,7 @@ class ConversationUsageListResp(BaseModel):
msg (str): Response message
data (ConversationUsageList): Conversation usage data
"""
code: int
msg: str
data: ConversationUsageList

View file

@ -18,6 +18,7 @@ class AssistantSupport(BaseModel):
tools (List[str]): List of tool names supported by the assistant
starter_prompts (List[str]): Recommended startup prompts for the assistant
"""
assistant_id: str
created_at: int
updated_at: int
@ -38,6 +39,7 @@ class AssistantSupportResp(BaseModel):
msg (str): Response message
data (List[AssistantSupport]): List of available assistants
"""
code: int
msg: str
data: List[AssistantSupport]

View file

@ -21,6 +21,7 @@ class AudioCustomizationParam(TypedDict, total=False):
platform will generate default if not provided by client
user_id (str): User ID
"""
model: str
input: str
voice_text: str

View file

@ -21,6 +21,7 @@ class AudioSpeechParams(TypedDict, total=False):
platform will generate default if not provided by client
user_id (str): User ID
"""
model: str
input: str
voice: str

View file

@ -14,6 +14,7 @@ class AsyncTaskStatus(BaseModel):
model (Optional[str]): Model used for the task
task_status (Optional[str]): Current status of the task
"""
id: Optional[str] = None
request_id: Optional[str] = None
model: Optional[str] = None
@ -32,6 +33,7 @@ class AsyncCompletion(BaseModel):
choices (List[CompletionChoice]): List of completion choices
usage (CompletionUsage): Token usage statistics
"""
id: Optional[str] = None
request_id: Optional[str] = None
model: Optional[str] = None

View file

@ -11,6 +11,7 @@ class Function(BaseModel):
arguments: Function call arguments
name: Function name
"""
arguments: str
name: str
@ -24,6 +25,7 @@ class CompletionMessageToolCall(BaseModel):
function: Function call information
type: Type of the tool call
"""
id: str
function: Function
type: str
@ -39,6 +41,7 @@ class CompletionMessage(BaseModel):
reasoning_content: Reasoning content
tool_calls: List of tool calls in the message
"""
content: Optional[str] = None
role: str
reasoning_content: Optional[str] = None
@ -52,8 +55,10 @@ class PromptTokensDetails(BaseModel):
Attributes:
cached_tokens: Number of tokens reused from cache
"""
cached_tokens: int
class CompletionTokensDetails(BaseModel):
"""
Detailed breakdown of token usage for the model completion
@ -61,8 +66,10 @@ class CompletionTokensDetails(BaseModel):
Attributes:
reasoning_tokens: Number of tokens used for reasoning steps
"""
reasoning_tokens: int
class CompletionUsage(BaseModel):
"""
Token usage information for completion
@ -74,6 +81,7 @@ class CompletionUsage(BaseModel):
completion_tokens_details: Detailed breakdown of token usage for the model completion
total_tokens: Total number of tokens used
"""
prompt_tokens: int
prompt_tokens_details: Optional[PromptTokensDetails] = None
completion_tokens: int
@ -90,6 +98,7 @@ class CompletionChoice(BaseModel):
finish_reason: Reason why the completion finished
message: Completion message
"""
index: int
finish_reason: str
message: CompletionMessage
@ -107,6 +116,7 @@ class Completion(BaseModel):
id: Unique identifier for the completion
usage: Token usage information
"""
model: Optional[str] = None
created: Optional[int] = None
choices: List[CompletionChoice]

View file

@ -11,6 +11,7 @@ class ChoiceDeltaFunctionCall(BaseModel):
arguments: Function call arguments
name: Function name
"""
arguments: Optional[str] = None
name: Optional[str] = None
@ -23,6 +24,7 @@ class ChoiceDeltaToolCallFunction(BaseModel):
arguments: Function call arguments
name: Function name
"""
arguments: Optional[str] = None
name: Optional[str] = None
@ -37,6 +39,7 @@ class ChoiceDeltaToolCall(BaseModel):
function: Function call information
type: Type of the tool call
"""
index: int
id: Optional[str] = None
function: Optional[ChoiceDeltaToolCallFunction] = None
@ -52,6 +55,7 @@ class AudioCompletionChunk(BaseModel):
data: Audio data content
expires_at: Timestamp when the audio expires
"""
id: Optional[str] = None
data: Optional[str] = None
expires_at: Optional[int] = None
@ -68,6 +72,7 @@ class ChoiceDelta(BaseModel):
tool_calls: List of tool call deltas
audio: Audio completion chunk
"""
content: Optional[str] = None
role: Optional[str] = None
reasoning_content: Optional[str] = None
@ -84,6 +89,7 @@ class Choice(BaseModel):
finish_reason: Reason why the completion finished
index: Index of the choice
"""
delta: ChoiceDelta
finish_reason: Optional[str] = None
index: int
@ -98,6 +104,7 @@ class CompletionUsage(BaseModel):
completion_tokens: Number of tokens in the completion
total_tokens: Total number of tokens used
"""
prompt_tokens: int
completion_tokens: int
total_tokens: int
@ -115,6 +122,7 @@ class ChatCompletionChunk(BaseModel):
usage: Token usage information
extra_json: Additional JSON data
"""
id: Optional[str] = None
choices: List[Choice]
created: Optional[int] = None

View file

@ -26,6 +26,7 @@ class FileCreateParams(TypedDict, total=False):
knowledge_id: When the file upload purpose is retrieval, you need to specify the knowledge base ID to upload
sentence_size: Sentence size parameter for retrieval purpose uploads
"""
file: FileTypes
upload_detail: List[UploadDetail]
purpose: Required[Literal['fine-tune', 'retrieval', 'batch']]

View file

@ -17,6 +17,7 @@ class FileObject(BaseModel):
status (Optional[str]): Current status of the file
status_details (Optional[str]): Additional details about the file status
"""
id: Optional[str] = None
bytes: Optional[int] = None
created_at: Optional[int] = None
@ -36,6 +37,7 @@ class ListOfFileObject(BaseModel):
data (List[FileObject]): List of file objects
has_more (Optional[bool]): Whether there are more files available
"""
object: Optional[str] = None
data: List[FileObject]
has_more: Optional[bool] = None

View file

@ -11,5 +11,6 @@ class Completion(BaseModel):
model: The model used for moderation
input: The input content for moderation (can be string, list of strings, or dictionary)
"""
model: Optional[str] = None
input: Optional[Union[str, List[str], Dict]] = None

View file

@ -16,5 +16,6 @@ class SensitiveWordCheckRequest(TypedDict, total=False):
contact business to obtain corresponding permissions, otherwise the disable
setting will not take effect.
"""
type: Optional[str]
status: Optional[str]

View file

@ -16,6 +16,7 @@ class ChoiceDeltaToolCall(BaseModel):
search_recommend (Optional[SearchRecommend]): Search recommendations
type (Optional[str]): Type of the tool call
"""
index: int
id: Optional[str] = None
@ -33,6 +34,7 @@ class ChoiceDelta(BaseModel):
role (Optional[str]): The role of the message sender
tool_calls (Optional[List[ChoiceDeltaToolCall]]): List of tool call deltas
"""
role: Optional[str] = None
tool_calls: Optional[List[ChoiceDeltaToolCall]] = None
@ -46,6 +48,7 @@ class Choice(BaseModel):
finish_reason (Optional[str]): Reason why the generation finished
index (int): Index of this choice in the response
"""
delta: ChoiceDelta
finish_reason: Optional[str] = None
index: int
@ -60,6 +63,7 @@ class WebSearchChunk(BaseModel):
choices (List[Choice]): List of choices in this chunk
created (Optional[int]): Timestamp when the chunk was created
"""
id: Optional[str] = None
choices: List[Choice]
created: Optional[int] = None

View file

@ -28,6 +28,7 @@ class VideoCreateParams(TypedDict, total=False):
platform will generate default if not provided by client
user_id (str): User ID
"""
model: str
prompt: str
image_url: str | list[str] | dict

View file

@ -12,6 +12,7 @@ class SearchIntentResp(BaseModel):
intent (str): Determined intent type
keywords (str): Search keywords
"""
query: str
intent: str
keywords: str
@ -30,6 +31,7 @@ class SearchResultResp(BaseModel):
refer (str): Reference number [ref_1]
publish_date (str): Publish date
"""
title: str
link: str
content: str
@ -50,6 +52,7 @@ class WebSearchResp(BaseModel):
search_intent (Optional[SearchIntentResp]): Search intent response
search_result (Optional[SearchResultResp]): Search result response
"""
created: Optional[int] = None
request_id: Optional[str] = None
id: Optional[str] = None

View file

@ -3,7 +3,7 @@ import logging.config
import time
import zai
from zai import ZaiClient, ZhipuAiClient
from zai import ZaiClient
def test_completions_sync(logging_conf):

View file

@ -71,9 +71,12 @@ class TestZaiClientFileServer:
def test_delete_files(self, test_server):
try:
# Only delete files if they were successfully created
if test_server.file_id1:
delete1 = test_server.client.files.delete(file_id=test_server.file_id1)
print(delete1)
if test_server.file_id2:
delete2 = test_server.client.files.delete(file_id=test_server.file_id2)
print(delete2)

View file

@ -8,7 +8,13 @@ from zai import ZaiClient
def test_completions_temp0(logging_conf):
logging.config.dictConfig(logging_conf) # type: ignore
client = ZaiClient(disable_token_cache=False) # Fill in your own API Key
# Skip this test if no valid API key is provided
import os
if not os.environ.get('ZAI_API_KEY') or os.environ.get('ZAI_API_KEY') == '{your apikey}':
import pytest
pytest.skip("No valid API key provided for integration test")
client = ZaiClient(disable_token_cache=False)
try:
# Generate request_id
request_id = time.time()

View file

@ -5,6 +5,7 @@ import time
import zai
from zai import ZaiClient
def test_chat_completion_with_thinking(logging_conf):
logging.config.dictConfig(logging_conf) # type: ignore
client = ZaiClient() # Fill in your own API Key
@ -15,9 +16,7 @@ def test_chat_completion_with_thinking(logging_conf):
response = client.chat.completions.create(
request_id=request_id,
model='glm-4.5',
messages=[
{'role': 'user', 'content': '请介绍一下Agent的原理并给出详细的推理过程'}
],
messages=[{'role': 'user', 'content': '请介绍一下Agent的原理并给出详细的推理过程'}],
top_p=0.7,
temperature=0.9,
)
@ -30,6 +29,7 @@ def test_chat_completion_with_thinking(logging_conf):
except zai.core._errors.APIStatusError as err:
print(err)
def test_chat_completion_without_thinking(logging_conf):
logging.config.dictConfig(logging_conf) # type: ignore
client = ZaiClient() # Fill in your own API Key
@ -40,14 +40,12 @@ def test_chat_completion_without_thinking(logging_conf):
response = client.chat.completions.create(
request_id=request_id,
model='glm-4.5',
messages=[
{'role': 'user', 'content': '请介绍一下Agent的原理'}
],
messages=[{'role': 'user', 'content': '请介绍一下Agent的原理'}],
top_p=0.7,
temperature=0.9,
thinking={
"type": "disabled",
}
'type': 'disabled',
},
)
print(response)

View file

@ -132,7 +132,7 @@ def test_response_chat_model_cast(R: Type[BaseModel]) -> None:
assert model.id == 'completion123'
assert model.request_id == 'request456'
assert model.model == 'model-name'
assert model.created == None
assert model.created is None
assert isinstance(model.choices, list)
assert isinstance(model.choices[0], ChatCompletionChoice)
assert model.choices[0].index == 0
@ -303,7 +303,7 @@ def test_response_file_list_model_cast(R: Type[BaseModel]) -> None:
assert model.data[0].purpose == 'example purpose'
assert model.data[0].status == 'uploaded'
assert model.data[0].status_details == 'File uploaded successfully'
assert model.has_more == True
assert model.has_more
@pytest.mark.parametrize('R', [ImagesResponded])

View file

@ -1,35 +1,39 @@
from zai.types.agents.agents_completion import AgentsCompletion, AgentsError, AgentsCompletionChoice, AgentsCompletionMessage, AgentsCompletionUsage
from zai.types.agents.agents_completion import (
AgentsCompletion,
AgentsCompletionChoice,
AgentsCompletionMessage,
AgentsCompletionUsage,
AgentsError,
)
def test_agents_completion_error_field():
# 构造一个 AgentsError
error = AgentsError(code="404", message="Not Found")
error = AgentsError(code='404', message='Not Found')
# 构造一个完整的 AgentsCompletion
completion = AgentsCompletion(
agent_id="test_agent",
conversation_id="conv_1",
status="failed",
agent_id='test_agent',
conversation_id='conv_1',
status='failed',
choices=[
AgentsCompletionChoice(
index=0,
finish_reason="error",
message=AgentsCompletionMessage(content="error", role="system")
index=0, finish_reason='error', message=AgentsCompletionMessage(content='error', role='system')
)
],
request_id="req_1",
id="id_1",
request_id='req_1',
id='id_1',
usage=AgentsCompletionUsage(prompt_tokens=1, completion_tokens=1, total_tokens=2),
error=error
error=error,
)
# 检查 error 字段是否为 AgentsError 实例
assert isinstance(completion.error, AgentsError)
assert completion.error.code == "404"
assert completion.error.message == "Not Found"
assert completion.error.code == '404'
assert completion.error.message == 'Not Found'
# 检查序列化
as_dict = completion.model_dump()
assert as_dict["error"]["code"] == "404"
assert as_dict["error"]["message"] == "Not Found"
print("test_agents_completion_error_field passed.")
assert as_dict['error']['code'] == '404'
assert as_dict['error']['message'] == 'Not Found'
print('test_agents_completion_error_field passed.')