Skip to content

Commit 5cea230

Browse files
author
luk384090-cloud
committed
refactor(v2): Refactor configuration and session management modules
- Migrate save_api_key function from common module to v2._config module - Update comments to reflect the relocation of key-file authentication implementation - Move SDK session ID implementation to shared module dashscope._shared - Modify v2 package re-export strategy, no longer importing save_api_key from 1.x - Adjust incremental output parameter check logic in chat completion module - Update related tests to verify new module structure and functionality migration
1 parent 4d3e950 commit 5cea230

10 files changed

Lines changed: 102 additions & 57 deletions

File tree

‎dashscope/common/_sdk_session.py‎

Lines changed: 5 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,11 @@
11
# -*- coding: utf-8 -*-
22
# Copyright (c) Alibaba, Inc. and its affiliates.
3-
"""Single process-wide SDK session id.
3+
"""Compatibility re-export; the implementation moved to ``dashscope._shared``.
44
5-
The ``x-dashscope-sdk-session-id`` header lets server-side access logs
6-
group every request one SDK session makes. A process can emit requests
7-
through both stacks at once -- the 1.x API classes and the 2.0 transport
8-
-- and the 2.0 client even delegates some calls (rl, agentstudio)
9-
straight into 1.x. If each stack minted its own id, one logical session
10-
would split across several ids and the grouping breaks.
11-
12-
This leaf module is the one source of truth both import. It depends only
13-
on the stdlib so it can be pulled in from anywhere without dragging
14-
``dashscope/__init__`` (and its v2 surface) into a cycle.
15-
16-
The embedded ``acli`` deliberately keeps its own id: it is a one-way
17-
sync copy of the agenticCLI repo (design doc §3.2), so wiring it to this
18-
module would be silently reverted by the next sync and cannot be
19-
upstreamed as written.
5+
The session id state lives only in ``dashscope._shared._sdk_session`` so a
6+
process keeps exactly one id. Import from there in new code.
207
"""
218

22-
from __future__ import annotations
23-
24-
import threading
25-
import uuid
26-
27-
_session_id = None
28-
_lock = threading.Lock()
29-
30-
31-
def sdk_session_id() -> str:
32-
"""Return this process's session id, generating it on first use.
9+
from dashscope._shared._sdk_session import sdk_session_id
3310

34-
Double-checked locking: concurrent callers must not each mint their
35-
own id and break the one-id-per-process invariant log grouping
36-
relies on.
37-
"""
38-
global _session_id # pylint: disable=global-statement
39-
if _session_id is None:
40-
with _lock:
41-
if _session_id is None:
42-
_session_id = uuid.uuid4().hex
43-
return _session_id
11+
__all__ = ["sdk_session_id"]

‎dashscope/common/utils.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@
1515
import aiohttp
1616
import requests
1717

18+
from dashscope._shared._sdk_session import sdk_session_id
1819
from dashscope.api_entities.dashscope_response import DashScopeAPIResponse
19-
from dashscope.common._sdk_session import sdk_session_id
2020
from dashscope.common.api_key import get_default_api_key
2121
from dashscope.common.constants import SSE_CONTENT_TYPE
2222
from dashscope.common.logging import logger
2323
from dashscope.version import __version__
2424

2525
# Shared with the 2.0 transport so one process reports one
26-
# x-dashscope-sdk-session-id across both stacks (see _sdk_session;
26+
# x-dashscope-sdk-session-id across both stacks (see _shared._sdk_session;
2727
# acli deliberately keeps its own, it is a one-way sync copy).
2828
_SDK_SESSION_ID = sdk_session_id()
2929

‎dashscope/v2/__init__.py‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,15 @@
77
"""
88

99
from dashscope.v2 import errors
10-
from dashscope.v2._config import Config, Settings, settings
10+
from dashscope.v2._config import Config, Settings, save_api_key, settings
1111
from dashscope.v2._quickstart import chat
1212
from dashscope.v2.client import AsyncClient, Client
1313
from dashscope.v2.errors import DashScopeError, ErrorCode
1414
from dashscope.v2.transport.ws_connection import WsConnection, WsTask
1515

16-
# Offline helpers delegated to the 1.x implementation, re-exported so the
17-
# 2.0 namespace is self-contained (same objects the package root already
18-
# exposes): the local tiktoken wrapper and the key-file writer.
19-
from dashscope.common.api_key import save_api_key # noqa: E402
16+
# Offline helper still delegated to the 1.x implementation, re-exported so
17+
# the 2.0 namespace is self-contained (same objects the package root
18+
# already exposes): the local tiktoken wrapper.
2019
from dashscope.tokenizers import ( # noqa: E402
2120
get_tokenizer,
2221
list_tokenizers,

‎dashscope/v2/_config.py‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,8 @@
136136
_LOG_ENV = "DASHSCOPE_LOG"
137137
_CONFIG_PATH_ENV = "DASHSCOPE_CONFIG"
138138
_DEFAULT_CONFIG_PATH = "~/.dashscope/config.toml"
139-
# 1.x key-file auth (dashscope.save_api_key): env-specified path first,
140-
# then the default file -- the lowest api_key tier.
139+
# Key-file auth (save_api_key, below): env-specified path first, then the
140+
# default file -- the lowest api_key tier.
141141
_API_KEY_FILE_PATH_ENV = "DASHSCOPE_API_KEY_FILE_PATH"
142142
_DEFAULT_API_KEY_FILE_PATH = "~/.dashscope/api_key"
143143

@@ -542,6 +542,26 @@ def _api_key_from_file() -> Optional[str]:
542542
return None
543543

544544

545+
def save_api_key(
546+
api_key: str, api_key_file_path: Optional[str] = None
547+
) -> None:
548+
"""Store an api key in the key file ``_api_key_from_file`` reads back.
549+
550+
Without an explicit path this writes ``~/.dashscope/api_key``, the same
551+
fixed location the 1.x ``dashscope.save_api_key`` uses, so a key stored
552+
through either stack is picked up by both. As in 1.x,
553+
``DASHSCOPE_API_KEY_FILE_PATH`` redirects only the read, not the write.
554+
"""
555+
target = (
556+
_DEFAULT_API_KEY_FILE_PATH
557+
if api_key_file_path is None
558+
else api_key_file_path
559+
)
560+
path = Path(target).expanduser()
561+
path.parent.mkdir(parents=True, exist_ok=True)
562+
path.write_text(api_key, encoding="utf-8")
563+
564+
545565
def _pick(
546566
explicit: Any,
547567
settings_value: Any,

‎dashscope/v2/resources/chat/completions.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414

1515
from typing_extensions import Literal
1616

17-
from dashscope.utils.param_utils import ParamUtil
1817
from dashscope.v2.errors import response_error
1918
from dashscope.v2.protocols.base import ChatRequest
2019
from dashscope.v2.resources._oss import (
2120
OSS_RESOLVE_HEADERS,
2221
upload_message_media,
2322
)
23+
from dashscope.v2.resources.chat._params import (
24+
should_modify_incremental_output,
25+
)
2426
from dashscope.v2.types.common import Usage, with_headers
2527
from dashscope.v2.types.completions import (
2628
ChatCompletion,
@@ -114,7 +116,7 @@ def _build_request(
114116
merge_incremental = False
115117
if stream and incremental_output is not None:
116118
if client.protocol.name == "native":
117-
server_side = not ParamUtil.should_modify_incremental_output(model)
119+
server_side = not should_modify_incremental_output(model)
118120
if incremental_output is False and not server_side:
119121
merge_incremental = True
120122
# 1.x also forces the wire incremental while merging

‎dashscope/v2/transport/_ident.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
# One process-wide session id shared with the 1.x API classes, so a
2121
# single SDK session is not split across ids when the 2.0 client
2222
# delegates calls (rl, agentstudio) into 1.x. (acli keeps its own: it is
23-
# a one-way sync copy, see common/_sdk_session.)
24-
from dashscope.common._sdk_session import sdk_session_id
23+
# a one-way sync copy, see _shared/_sdk_session.)
24+
from dashscope._shared._sdk_session import sdk_session_id
2525

2626
SDK_CLIENT_HEADER = "x-dashscope-sdk-client"
2727
SDK_SESSION_ID_HEADER = "x-dashscope-sdk-session-id"

‎tests/unit/test_sdk_headers.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,20 @@ def test_python_sdk_client_header():
5959
assert headers[SESSION_HEADER] == _SDK_SESSION_ID
6060

6161

62+
def test_session_id_has_one_source_across_stacks():
63+
# A process using the 1.x classes and the 2.0 client at once must
64+
# report one id, or server-side log grouping splits a single logical
65+
# session. The legacy deep path re-exports the neutral leaf rather
66+
# than holding its own copy of the state.
67+
from dashscope._shared._sdk_session import sdk_session_id as leaf
68+
from dashscope.common._sdk_session import sdk_session_id as via_1x
69+
from dashscope.v2.transport._ident import sdk_session_id as via_v2
70+
71+
assert via_1x is leaf
72+
assert via_v2 is leaf
73+
assert leaf() == _SDK_SESSION_ID
74+
75+
6276
def test_acli_client_header():
6377
# pylint: disable=protected-access
6478
print("\n=== acli: x-dashscope-sdk-client ===")

‎tests/v2/test_config.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,38 @@ def test_save_api_key_roundtrip(clean_env, tmp_path, monkeypatch):
462462
assert cfg.api_key == "sk-saved"
463463

464464

465+
def test_save_api_key_writes_the_default_path(
466+
clean_env,
467+
tmp_path,
468+
monkeypatch,
469+
):
470+
from dashscope.v2 import _config
471+
472+
default = tmp_path / "home" / ".dashscope" / "api_key"
473+
monkeypatch.setattr(_config, "_DEFAULT_API_KEY_FILE_PATH", str(default))
474+
# The env var outranks the default path when reading; drop it so this
475+
# exercises the write-default / read-default pair.
476+
monkeypatch.delenv("DASHSCOPE_API_KEY_FILE_PATH", raising=False)
477+
478+
_config.save_api_key("sk-default")
479+
480+
assert default.read_text(encoding="utf-8") == "sk-default"
481+
cfg = resolve_config(config_path="/nonexistent/config.toml")
482+
assert cfg.api_key == "sk-default"
483+
484+
485+
def test_save_api_key_accepts_a_bare_filename(
486+
clean_env, tmp_path, monkeypatch
487+
):
488+
from dashscope.v2 import save_api_key
489+
490+
# 1.x does os.makedirs(os.path.dirname(path)), which raises when the
491+
# path has no directory component; the v2 writer resolves against cwd.
492+
monkeypatch.chdir(tmp_path)
493+
save_api_key("sk-bare", "api_key")
494+
assert (tmp_path / "api_key").read_text(encoding="utf-8") == "sk-bare"
495+
496+
465497
def test_invalid_protocol(clean_env):
466498
with pytest.raises(ValueError, match="invalid protocol"):
467499
resolve_config(protocol="grpc", config_path="/nonexistent/config.toml")

‎tests/v2/test_package_root.py‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,21 +50,31 @@ def test_legacy_exports_untouched():
5050
assert hasattr(dashscope, "save_api_key")
5151

5252

53-
def test_v2_namespace_reexports_1x_offline_helpers():
54-
# The 2.0 namespace delegates the offline helpers to 1.x instead of
55-
# re-implementing them (same objects the package root exposes).
53+
def test_v2_namespace_reexports_1x_tokenizers():
54+
# The local tiktoken wrapper is still delegated to 1.x rather than
55+
# re-implemented (same objects the package root exposes).
5656
from dashscope import v2
57-
from dashscope.common.api_key import save_api_key as save_1x
5857
from dashscope.tokenizers import get_tokenizer as get_1x
5958
from dashscope.tokenizers import list_tokenizers as list_1x
6059

61-
assert v2.save_api_key is save_1x
6260
assert v2.get_tokenizer is get_1x
6361
assert v2.list_tokenizers is list_1x
64-
for name in ("get_tokenizer", "list_tokenizers", "save_api_key"):
62+
for name in ("get_tokenizer", "list_tokenizers"):
6563
assert name in v2.__all__
6664

6765

66+
def test_v2_owns_save_api_key():
67+
# The key-file writer is v2-native, so reaching for it does not pull in
68+
# the 1.x package root; the package root keeps its own 1.x writer.
69+
from dashscope import v2
70+
from dashscope.common.api_key import save_api_key as save_1x
71+
72+
assert v2.save_api_key.__module__ == "dashscope.v2._config"
73+
assert v2.save_api_key is not save_1x
74+
assert dashscope.save_api_key is save_1x
75+
assert "save_api_key" in v2.__all__
76+
77+
6878
def test_v2_get_tokenizer_encodes_locally():
6979
from dashscope import v2
7080
from dashscope.common.error import UnsupportedModel

‎tests/v2/test_transport.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ async def test_async_sdk_identification_headers(no_sleep):
430430

431431

432432
def test_sdk_session_id_cached_per_process():
433-
from dashscope.common import _sdk_session
433+
from dashscope._shared import _sdk_session
434434
from dashscope.v2.transport import _ident
435435

436436
original = _sdk_session._session_id

0 commit comments

Comments
 (0)