Skip to content

feat(dashscope): support DASHSCOPE_PROXY_BASE_URL for prompt cache via API gateway#3991

Merged
wenshao merged 12 commits into
QwenLM:mainfrom
HeZiGang:main
May 12, 2026
Merged

feat(dashscope): support DASHSCOPE_PROXY_BASE_URL for prompt cache via API gateway#3991
wenshao merged 12 commits into
QwenLM:mainfrom
HeZiGang:main

Conversation

@HeZiGang

@HeZiGang HeZiGang commented May 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add DASHSCOPE_PROXY_BASE_URL environment variable to identify API Gateway proxy URLs
  • Modify isDashScopeProvider() to detect proxy URL and enable DashScope prompt caching when baseUrl matches the env var
  • Add tests for proxy URL matching scenarios (matching, non-matching, trailing slash)

Test plan

  • All 43 tests in dashscope.test.ts pass
  • Verify prompt caching works with your proxy URL by setting DASHSCOPE_PROXY_BASE_URL=https://your-proxy.com/dashscope

Comment thread packages/core/src/core/openaiContentGenerator/provider/dashscope.ts Fixed

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] 尾部斜杠归一化不对称

baseUrl 的尾部斜杠在 L37-39 被剥离,但 DASHSCOPE_PROXY_BASE_URL 未做同样处理。用户设置 DASHSCOPE_PROXY_BASE_URL=https://proxy.com/dashscope/(带尾部斜杠)时,严格相等比较会失败,DashScope 功能(prompt 缓存、元数据、视觉模型支持)静默丢失。

Suggested change
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'];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] 测试环境变量泄露风险

3 个新 proxy 测试使用手动 save/restore 模式(const originalEnv = ...)。如果 expect() 断言抛出异常,restore 行不会执行,会污染后续测试。建议使用 vitest 的 vi.stubEnv()try/finally

Suggested change
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

@HeZiGang HeZiGang requested a review from wenshao May 11, 2026 06:22
@tanzhenxin tanzhenxin added the type/feature-request New feature or enhancement request label May 11, 2026

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ CI failing: CodeQL.

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.

Comment thread packages/core/src/core/openaiContentGenerator/constants.ts
Comment thread packages/core/src/core/openaiContentGenerator/provider/dashscope.ts
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Suggested change
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(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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(),
}));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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:

Suggested change
}));
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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is already implemented at lines 60–62. Why is this recommendation showing up?

@HeZiGang HeZiGang requested a review from wenshao May 12, 2026 02:42
// Matches: dashscope.aliyuncs.com, *.dashscope.aliyuncs.com, or *.dashscope-intl.aliyuncs.com
return /([\w-]+\.)?dashscope(-intl)?\.aliyuncs\.com/i.test(baseUrl);
const isDashscopeOrigin =
/([\w-]+\.)?dashscope(-intl)?\.aliyuncs\.com/i.test(normalizedBaseUrl);
buildRuntimeFetchOptions: vi.fn(),
}));

vi.mock('../constants.js', async (importOriginal) => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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 将漂移。

Suggested change
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

Comment thread packages/core/src/core/openaiContentGenerator/provider/dashscope.ts Outdated
);

if (normalizedProxyUrl && !isDashscopeOrigin && !isProxyConfigured) {
createDebugLogger('DashScopeOpenAICompatibleProvider').debug(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] 调试日志可能泄露嵌入 URL 的凭据 & 内联 createDebugLogger 违反代码库约定

两个问题在此行交汇:(1) 完整代理 URL 被写入调试日志文件,如果用户将凭据嵌入 URL(如 https://user:pass@proxy.com/dashscope),凭据将以明文持久化到磁盘(QWEN_DEBUG_LOG_FILE 默认启用)。(2) createDebugLogger 在方法内部内联调用,而代码库中 80+ 处调用均遵循模块级单例模式(const debugLogger = createDebugLogger('NAME'))。

Suggested change
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

HeZiGang and others added 2 commits May 12, 2026 12:48
…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.
@HeZiGang HeZiGang requested a review from wenshao May 12, 2026 04:59

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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'",
),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Suggested change
),
"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) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Suggested change
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.`,
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Suggested change
);
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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove the url in logs to prevent URL exposure.

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@HeZiGang HeZiGang requested a review from wenshao May 12, 2026 06:25
@wenshao

wenshao commented May 12, 2026

Copy link
Copy Markdown
Collaborator

Local verification report

Pulled e5bd8aa (latest HEAD), ran the targeted test suite, type-check, lint, and a subprocess-isolated real-world scenario suite.

Build + unit + lint

  • dashscope.test.ts: 44/44 pass
  • tsc --noEmit on packages/core: clean
  • eslint on the three changed files: clean

Earlier review items — confirmed addressed on e5bd8aa

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.com all recognized
  • api.openai.com and arbitrary proxy URLs rejected
  • QWEN_OAUTH always 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 /v1 vs request /v1/chat) correctly rejected
  • Official dashscope.aliyuncs.com URL 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 wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved after local verification — see comment above.

@wenshao wenshao merged commit 8e18e64 into QwenLM:main May 12, 2026
7 of 8 checks passed

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Downgraded from Approve to Comment: CI failing: CodeQL.

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(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Suggested change
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

tanzhenxin pushed a commit that referenced this pull request May 13, 2026
#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>
wenshao pushed a commit that referenced this pull request May 17, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type/feature-request New feature or enhancement request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants