feat(dashscope): support DASHSCOPE_PROXY_BASE_URL for prompt cache via API gateway#3991
Conversation
wenshao
left a comment
There was a problem hiding this comment.
[Suggestion] 缺少测试:DASHSCOPE_PROXY_BASE_URL 带尾部斜杠 + baseUrl 不带。现有测试仅覆盖 baseUrl 带斜杠的归一化情况,未覆盖环境变量带斜杠的逆向场景。建议补充测试用例覆盖此边界。
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| // Check if proxy is configured and matches | ||
| const isProxyConfigured = Boolean( | ||
| DASHSCOPE_PROXY_BASE_URL && | ||
| normalizedBaseUrl === DASHSCOPE_PROXY_BASE_URL, |
There was a problem hiding this comment.
[Critical] 尾部斜杠归一化不对称
baseUrl 的尾部斜杠在 L37-39 被剥离,但 DASHSCOPE_PROXY_BASE_URL 未做同样处理。用户设置 DASHSCOPE_PROXY_BASE_URL=https://proxy.com/dashscope/(带尾部斜杠)时,严格相等比较会失败,DashScope 功能(prompt 缓存、元数据、视觉模型支持)静默丢失。
| normalizedBaseUrl === DASHSCOPE_PROXY_BASE_URL, | |
| const normalizedProxyUrl = DASHSCOPE_PROXY_BASE_URL?.endsWith('/') | |
| ? DASHSCOPE_PROXY_BASE_URL.slice(0, -1) | |
| : DASHSCOPE_PROXY_BASE_URL; | |
| const isProxyConfigured = Boolean( | |
| normalizedProxyUrl && normalizedBaseUrl === normalizedProxyUrl, | |
| ); |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| }); | ||
|
|
||
| it('should return true when baseUrl matches DASHSCOPE_PROXY_BASE_URL', () => { | ||
| const originalEnv = process.env['DASHSCOPE_PROXY_BASE_URL']; |
There was a problem hiding this comment.
[Suggestion] 测试环境变量泄露风险
3 个新 proxy 测试使用手动 save/restore 模式(const originalEnv = ...)。如果 expect() 断言抛出异常,restore 行不会执行,会污染后续测试。建议使用 vitest 的 vi.stubEnv() 或 try/finally。
| const originalEnv = process.env['DASHSCOPE_PROXY_BASE_URL']; | |
| it('should return true when baseUrl matches DASHSCOPE_PROXY_BASE_URL', () => { | |
| vi.stubEnv('DASHSCOPE_PROXY_BASE_URL', 'https://your-proxy.com/dashscope'); | |
| const config = { | |
| authType: AuthType.USE_OPENAI, | |
| baseUrl: 'https://your-proxy.com/dashscope', | |
| } as ContentGeneratorConfig; | |
| const result = DashScopeOpenAICompatibleProvider.isDashScopeProvider(config); | |
| expect(result).toBe(true); | |
| }); |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
wenshao
left a comment
There was a problem hiding this comment.
Focused diff (+81/-1 across 3 files) correctly implements DASHSCOPE_PROXY_BASE_URL for dashscope prompt cache via API gateway. The trailing slash concern from the prior review has been addressed — both baseUrl and DASHSCOPE_PROXY_BASE_URL are now normalized.
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
wenshao
left a comment
There was a problem hiding this comment.
[Suggestion] Dead code: getDashScopeProxyBaseUrl() and speculative comments (constants.ts:9-14)
Both a const and a getter function are exported for the same env var. The function is never imported anywhere — it is dead code. The comments ("Consider exporting a getter function for SDK use:" / "Or a function:") read like design notes, not documentation. Remove the function and comments; keep only the const that production code actually uses:
export const DASHSCOPE_PROXY_BASE_URL =
process.env['DASHSCOPE_PROXY_BASE_URL'];| const normalizedProxyUrl = DASHSCOPE_PROXY_BASE_URL?.endsWith('/') | ||
| ? DASHSCOPE_PROXY_BASE_URL.slice(0, -1) | ||
| : DASHSCOPE_PROXY_BASE_URL; | ||
|
|
There was a problem hiding this comment.
[Suggestion] Case-sensitivity inconsistency between origin check and proxy check
The origin-domain regex on line 43 uses the /i (case-insensitive) flag, but the proxy URL comparison here uses strict ===. If DASHSCOPE_PROXY_BASE_URL and baseUrl differ only in casing (common in CI/CD environments where env vars and config files come from different sources), the proxy match silently fails and DashScope-specific headers, cache control, and session tracking are all skipped.
| const isProxyConfigured = Boolean( | |
| normalizedProxyUrl && | |
| normalizedBaseUrl.toLowerCase() === normalizedProxyUrl.toLowerCase(), | |
| ); |
— glm-5.1 via Qwen Code /review
| ? DASHSCOPE_PROXY_BASE_URL.slice(0, -1) | ||
| : DASHSCOPE_PROXY_BASE_URL; | ||
|
|
||
| const isProxyConfigured = Boolean( |
There was a problem hiding this comment.
[Suggestion] No diagnostic logging when proxy URL doesn't match
When DASHSCOPE_PROXY_BASE_URL is set but does not match baseUrl, the code silently falls through to the default provider. DashScope-specific headers (X-DashScope-CacheControl, X-DashScope-UserAgent, X-DashScope-AuthType), cache control, and session tracking are all skipped with zero indication of why. This is the hardest failure mode to debug — the user sees "prompt caching not working" with no error or warning. Consider adding a debug/trace-level log when the proxy URL is set but doesn't match.
— glm-5.1 via Qwen Code /review
| @@ -38,6 +38,20 @@ vi.mock('../../../utils/runtimeFetchOptions.js', () => ({ | |||
| buildRuntimeFetchOptions: vi.fn(), | |||
| })); | |||
There was a problem hiding this comment.
[Suggestion] Test mock diverges from production and is fragile
The vi.mock('../constants.js') uses a getter for DASHSCOPE_PROXY_BASE_URL (dynamic read from process.env), but production uses export const (frozen at import time). Additionally, the mock duplicates all 6 other constant exports verbatim — any future constant added to constants.ts would be silently undefined in these tests. Consider using importOriginal to preserve real exports:
| })); | |
| vi.mock('../constants.js', async (importOriginal) => { | |
| const actual = await importOriginal(); | |
| return { | |
| ...actual, | |
| get DASHSCOPE_PROXY_BASE_URL() { | |
| return process.env['DASHSCOPE_PROXY_BASE_URL']; | |
| }, | |
| }; | |
| }); |
— glm-5.1 via Qwen Code /review
There was a problem hiding this comment.
This logic is already implemented at lines 60–62. Why is this recommendation showing up?
…ule in the test file
| buildRuntimeFetchOptions: vi.fn(), | ||
| })); | ||
|
|
||
| vi.mock('../constants.js', async (importOriginal) => { |
There was a problem hiding this comment.
[Critical] 重复的 vi.mock 调用——第一个是死代码,导致 tsc 编译错误
对同一模块 ../constants.js 有两个 vi.mock 调用。第一个(L51-59, async (importOriginal))被第二个(L62-73, 硬编码所有常量)完全覆盖。第一个 mock 中的 ...actual 展开触发 TS2698: Spread types may only be created from object types。第二个 mock 硬编码了 DEFAULT_TIMEOUT: 120000 等值——如果 constants.ts 中真实常量发生变化,mock 将漂移。
| vi.mock('../constants.js', async (importOriginal) => { | |
| // 删除第一个 vi.mock 块(L51-59),删除第二个 mock 中的硬编码常量,改为: | |
| vi.mock('../constants.js', async (importOriginal) => { | |
| const actual = await importOriginal<typeof import('../constants.js')>(); | |
| return { | |
| ...actual, | |
| get DASHSCOPE_PROXY_BASE_URL() { | |
| return process.env['DASHSCOPE_PROXY_BASE_URL']; | |
| }, | |
| }; | |
| }); |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| ); | ||
|
|
||
| if (normalizedProxyUrl && !isDashscopeOrigin && !isProxyConfigured) { | ||
| createDebugLogger('DashScopeOpenAICompatibleProvider').debug( |
There was a problem hiding this comment.
[Suggestion] 调试日志可能泄露嵌入 URL 的凭据 & 内联 createDebugLogger 违反代码库约定
两个问题在此行交汇:(1) 完整代理 URL 被写入调试日志文件,如果用户将凭据嵌入 URL(如 https://user:pass@proxy.com/dashscope),凭据将以明文持久化到磁盘(QWEN_DEBUG_LOG_FILE 默认启用)。(2) createDebugLogger 在方法内部内联调用,而代码库中 80+ 处调用均遵循模块级单例模式(const debugLogger = createDebugLogger('NAME'))。
| createDebugLogger('DashScopeOpenAICompatibleProvider').debug( | |
| // 模块顶层: | |
| const debugLogger = createDebugLogger('DashScopeOpenAICompatibleProvider'); | |
| // 在 isDashScopeProvider 内: | |
| if (normalizedProxyUrl && !isDashscopeOrigin && !isProxyMatch) { | |
| debugLogger.debug( | |
| `DASHSCOPE_PROXY_BASE_URL is configured but the request baseUrl does not match. DashScope headers/cache control will be skipped.`, | |
| ); | |
| } |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
…pe.ts Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
- Updated logging format to match codebase guidelines. - Implemented URL masking in logs to mitigate data leakage risks.
wenshao
left a comment
There was a problem hiding this comment.
[Suggestion] Missing vi.unstubAllEnvs() in test cleanup (dashscope.test.ts, beforeEach block)
The three new proxy tests use vi.stubEnv() but beforeEach only calls vi.clearAllMocks(). All 7 other test files in the codebase that use stubEnv explicitly call vi.unstubAllEnvs() (e.g., shellExecutionService.test.ts, oauth-token-storage.test.ts). Without this cleanup, stubbed env vars can leak into other tests in the same describe block, causing order-dependent test failures.
Suggested fix: add vi.unstubAllEnvs(); after vi.clearAllMocks(); in beforeEach.
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| expect(mockDebugLogger.debug).toHaveBeenCalledWith( | ||
| expect.stringContaining( | ||
| "DASHSCOPE_PROXY_BASE_URL is configured as 'https://your-proxy.com/dashscope'", | ||
| ), |
There was a problem hiding this comment.
[Critical] Test assertion does not match production code — the test will fail.
The test expects the debug log to contain "DASHSCOPE_PROXY_BASE_URL is configured as 'https://your-proxy.com/dashscope'", but the actual code in dashscope.ts outputs "DASHSCOPE_PROXY_BASE_URL is configured but the request baseUrl does not match. DashScope headers/cache control will be skipped.".
The expect.stringContaining check will fail because the actual message uses "configured but" instead of "configured as", and does not include the URL value.
| ), | |
| "DASHSCOPE_PROXY_BASE_URL is configured but the request baseUrl does not match. DashScope headers/cache control will be skipped.", |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| ); | ||
|
|
||
| const debugLogger = createDebugLogger('DashScopeOpenAICompatibleProvider'); | ||
| if (normalizedProxyUrl && !isDashscopeOrigin && !isProxyMatch) { |
There was a problem hiding this comment.
[Suggestion] createDebugLogger called inside static method body — inconsistent with codebase convention.
The codebase has 100+ usages of createDebugLogger at module level (e.g., converter.ts, errorHandler.ts, openaiContentGenerator.ts). This is the only place it's called inside a method body, creating a new logger object on every invocation.
Move it to module level for consistency and to avoid unnecessary allocations.
| if (normalizedProxyUrl && !isDashscopeOrigin && !isProxyMatch) { | |
| // At module level (outside the class), after imports: | |
| const debugLogger = createDebugLogger('DashScopeOpenAICompatibleProvider'); |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| if (normalizedProxyUrl && !isDashscopeOrigin && !isProxyMatch) { | ||
| debugLogger.debug( | ||
| `DASHSCOPE_PROXY_BASE_URL is configured but the request baseUrl does not match. DashScope headers/cache control will be skipped.`, | ||
| ); |
There was a problem hiding this comment.
[Suggestion] Debug log on proxy mismatch does not include the actual URL values being compared.
When DASHSCOPE_PROXY_BASE_URL is configured but doesn't match, the current log message does not show either the proxy URL or the request baseUrl. Users cannot diagnose why the match failed without adding their own logging.
| ); | |
| debugLogger.debug( | |
| `DASHSCOPE_PROXY_BASE_URL is configured as '${normalizedProxyUrl}' ` + | |
| `but the request baseUrl ('${normalizedBaseUrl}') does not match. ` + | |
| `DashScope headers/cache control will be skipped.`, | |
| ); |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
There was a problem hiding this comment.
remove the url in logs to prevent URL exposure.
wenshao
left a comment
There was a problem hiding this comment.
No new findings beyond the existing review. The test assertion mismatch at dashscope.test.ts was already flagged in a prior review comment. All other review dimensions (correctness, security, code quality, performance) found no additional issues in this diff.
The outstanding Critical issues (trailing slash normalization asymmetry, duplicate vi.mock) and Suggestions from the previous review round still need attention.
— DeepSeek/deepseek-v4-pro via Qwen Code /review
…llocations. 2. Fix the UT in dashscope.ts.
Local verification reportPulled Build + unit + lint
Earlier review items — confirmed addressed on
|
| Item | Status |
|---|---|
| [Critical] Test assertion did not match production log string | ✅ Fixed — dashscope.test.ts:240 now matches the dashscope.ts:60 template literal exactly |
[Critical] Duplicate vi.mock('../constants.js', ...) causing TS2698 Spread types |
✅ Fixed — only one mock remains (line 52), using a get accessor so process.env['DASHSCOPE_PROXY_BASE_URL'] is re-read per call |
[Suggestion] createDebugLogger inline vs codebase convention |
✅ Fixed — moved to module level (dashscope.ts:22) |
[Suggestion] isProxyConfigured misleading name |
✅ Fixed — renamed to isProxyMatch (line 53) |
[Suggestion] Missing vi.unstubAllEnvs() in beforeEach |
✅ Fixed — added at dashscope.test.ts:72 |
| [Suggestion] Include URL values in debug log for diagnosis | ⚖️ Trade-off accepted: author intentionally omitted URLs to prevent credential leak (https://user:pass@host/... would be persisted to disk under QWEN_DEBUG_LOG_FILE). The static log message is still actionable because there is exactly one mismatch code path. |
Real-world scenarios (17/17)
constants.ts reads process.env['DASHSCOPE_PROXY_BASE_URL'] at module-load time, so each scenario runs in a fresh node subprocess to exercise the production path with a different env value.
Baseline (no env) — 6/6
dashscope.aliyuncs.com,dashscope-intl.aliyuncs.com,coding.dashscope.aliyuncs.comall recognizedapi.openai.comand arbitrary proxy URLs rejectedQWEN_OAUTHalways recognized regardless of baseUrl
Proxy match — 11/11
- Exact match → DashScope semantics enabled
- Non-match → returns
false - Trailing slash normalized on either or both sides
- Case-insensitive comparison confirmed working both directions
- Path-prefix mismatch (e.g. proxy
/v1vs request/v1/chat) correctly rejected - Official
dashscope.aliyuncs.comURL still works when proxy env is set - Empty-string env behaves as unset (does not break official URL detection)
Debug-log credential safety
Confirmed by reading dashscope.ts:59-61: the log message is a pure template literal with no ${normalizedBaseUrl} or ${normalizedProxyUrl} interpolation. URL/credential leakage is structurally impossible.
Note on CI
GitHub's standalone CodeQL run flags a "Polynomial regular expression used on uncontrolled data" alert at dashscope.ts:44. That regex (/([\w-]+\.)?dashscope(-intl)?\.aliyuncs\.com/i) is pre-existing on main — this PR does not modify it (main:37 has the same line). The proxy-match code added by this PR uses toLowerCase() === comparison and introduces no new regex. The alert can be tracked independently from this PR.
LGTM. Merging.
wenshao
left a comment
There was a problem hiding this comment.
Approved after local verification — see comment above.
wenshao
left a comment
There was a problem hiding this comment.
No critical issues found. One suggestion for proxy URL matching robustness. LGTM overall! ✅
| ? DASHSCOPE_PROXY_BASE_URL.slice(0, -1) | ||
| : DASHSCOPE_PROXY_BASE_URL; | ||
|
|
||
| const isProxyMatch = Boolean( |
There was a problem hiding this comment.
[Suggestion] Proxy URL comparison uses exact equality — may miss path prefix mismatches
The proxy match uses normalizedBaseUrl.toLowerCase() === normalizedProxyUrl.toLowerCase() (strict full-URL equality). If a user sets DASHSCOPE_PROXY_BASE_URL=https://proxy.com/dashscope but their baseUrl includes a versioned path like https://proxy.com/dashscope/v1, the match fails silently. DashScope-specific behavior (cache headers, metadata) would be dropped with no error — only a debug-level log that most users won't see.
The origin-domain check on line 46 uses a lenient regex substring match, so it's immune to this. The asymmetry is intentional but the strict proxy comparison could cause real misconfiguration issues.
| const isProxyMatch = Boolean( | |
| const isProxyMatch = Boolean(normalizedProxyUrl && (() => { | |
| try { | |
| const base = new URL(normalizedBaseUrl); | |
| const proxy = new URL(normalizedProxyUrl); | |
| return base.origin.toLowerCase() === proxy.origin.toLowerCase() && | |
| (base.pathname === proxy.pathname || | |
| base.pathname.startsWith( | |
| proxy.pathname.endsWith('/') ? proxy.pathname : proxy.pathname + '/', | |
| )); | |
| } catch { | |
| return false; | |
| } | |
| })()); |
— glm-5.1 via Qwen Code /review
#4112) CodeQL flagged a polynomial regular-expression alert on the unanchored /([\w-]+\.)?dashscope(-intl)?\.aliyuncs\.com/i pattern introduced in #3991 — repeated '-' in the baseUrl could trigger catastrophic backtracking. The unanchored regex was also too permissive: it would incidentally match the dashscope domain appearing anywhere in a URL path, not just in the hostname. Parse the baseUrl with the URL constructor and compare hostname suffixes instead. This eliminates the ReDoS surface entirely and is strictly tighter — only real dashscope[-intl].aliyuncs.com hostnames (and their subdomains) match. Co-authored-by: Qwen-Coder <noreply@alibaba-inc.com>
…a API gateway (#3991) Adds `DASHSCOPE_PROXY_BASE_URL` env var so DashScope prompt-cache headers fire when requests go through an internal API gateway proxy URL. - New constant `DASHSCOPE_PROXY_BASE_URL` in `openaiContentGenerator/constants.ts`, read once at module load from `process.env`. - `isDashScopeProvider()` now also returns true when `baseUrl` matches the configured proxy URL (case-insensitive, trailing slash on either side normalized). - Debug log fires on the configured-but-mismatched path with a static message (no URL interpolation, so embedded credentials in the proxy URL cannot leak to `QWEN_DEBUG_LOG_FILE`). - 9 new test cases covering match, mismatch, debug-log assertion, and trailing-slash normalization. Uses `vi.stubEnv` + `vi.unstubAllEnvs` in `beforeEach`. Closes #3991
Summary
DASHSCOPE_PROXY_BASE_URLenvironment variable to identify API Gateway proxy URLsisDashScopeProvider()to detect proxy URL and enable DashScope prompt caching when baseUrl matches the env varTest plan
DASHSCOPE_PROXY_BASE_URL=https://your-proxy.com/dashscope