@gajae-code/ai 0.12.4 → 0.12.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.12.5] - 2026-07-30
6
+ ### Fixed
7
+
8
+ - Alibaba Token Plan requests now carry Qwen Code's canonical DashScope request fingerprint on both transports. The built-in `alibaba-token-plan` provider (openai-responses `qwen3.8-max-preview` and openai-completions `glm-5.2`/`deepseek-v4-pro`) now emits the four upstream identity/cache/auth headers (`User-Agent`, `X-DashScope-CacheControl: enable`, `X-DashScope-UserAgent`, `X-DashScope-AuthType: openai`) matching `QwenLM/qwen-code` v0.21.1 (commit `f4cd6e1`) exactly, via a shared helper. DashScope is compatibility-sensitive to this client fingerprint, so a non-identical set can cause request instability and affect first-event latency. Caller headers still win per key (upstream `{...default, ...customHeaders}` precedence); non-Alibaba providers are byte-unchanged (#3557).
9
+
10
+ ### Added
11
+
12
+ - Reproducible Alibaba Token Plan header-parity A/B latency benchmark (`packages/ai/scripts/alibaba-token-plan-latency-ab.ts`): a fixed-seed interleaved A/B comparison of legacy vs Qwen-identical headers against a deterministic local HTTP server, reporting n/success/error/timeout and TTFT/total latency median/p90/p95/mean/stddev. No live credentials are required; a public-safe blocked-live-data receipt is included (`packages/ai/test/fixtures/alibaba-token-plan-latency-blocked-receipt.md`) (#3557).
13
+
14
+
5
15
  ## [0.12.4] - 2026-07-30
6
16
 
7
17
  ### Fixed
@@ -0,0 +1,57 @@
1
+ /**
2
+ * DashScope Token Plan canonical request headers.
3
+ *
4
+ * Reproduces QwenLM/qwen-code's DashScopeOpenAICompatibleProvider.buildHeaders()
5
+ * defaultHeaders so the built-in `alibaba-token-plan` provider emits the same
6
+ * client identity / cache / auth-type fingerprint upstream sends. DashScope is
7
+ * compatibility-sensitive to this fingerprint; a non-identical set can cause
8
+ * request instability and affect first-event latency (gajae-code #3557).
9
+ *
10
+ * Upstream pin (reproduce EXACTLY here):
11
+ * Repository: QwenLM/qwen-code
12
+ * Commit: f4cd6e1d8bbb1c24e7e5d1a40187d8e28aa7c4fb
13
+ * Version: 0.21.1
14
+ * Source: packages/core/src/core/openaiContentGenerator/provider/dashscope.ts
15
+ * buildHeaders():
16
+ * const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`;
17
+ * const defaultHeaders = {
18
+ * 'User-Agent': userAgent,
19
+ * 'X-DashScope-CacheControl': 'enable',
20
+ * 'X-DashScope-UserAgent': userAgent,
21
+ * 'X-DashScope-AuthType': authType,
22
+ * };
23
+ * return customHeaders ? { ...defaultHeaders, ...customHeaders } : defaultHeaders;
24
+ *
25
+ * The Token Plan preset authenticates with AuthType.USE_OPENAI ('openai'), so
26
+ * X-DashScope-AuthType is the constant 'openai'. Pin the version so an upstream
27
+ * bump is an explicit parity update rather than silent drift.
28
+ */
29
+ export declare const QWEN_CODE_UPSTREAM_REPO = "QwenLM/qwen-code";
30
+ export declare const QWEN_CODE_UPSTREAM_COMMIT = "f4cd6e1d8bbb1c24e7e5d1a40187d8e28aa7c4fb";
31
+ export declare const QWEN_CODE_UPSTREAM_VERSION = "0.21.1";
32
+ /**
33
+ * The Qwen Code CLI version string used in identity headers. Pinned to the
34
+ * upstream version at {@link QWEN_CODE_UPSTREAM_COMMIT}; change both together
35
+ * as an explicit parity update.
36
+ */
37
+ export declare function qwenCodeUserAgent(version?: string): string;
38
+ /**
39
+ * Canonical DashScope Token Plan headers (upstream defaultHeaders, no caller
40
+ * overrides applied). Exposed for tests/fixtures so the pinned wire set lives
41
+ * in exactly one place.
42
+ */
43
+ export declare function dashscopeTokenPlanDefaultHeaders(version?: string): Readonly<Record<string, string>>;
44
+ /**
45
+ * Merge canonical DashScope Token Plan identity headers onto a caller's header
46
+ * map, reproducing upstream buildHeaders() precedence EXACTLY:
47
+ * `{ ...defaultHeaders, ...customHeaders }` — caller wins per header.
48
+ *
49
+ * This mirrors GJC's existing kimi-code injection order
50
+ * (`headers = { ...getKimiCommonHeaders(), ...headers }`): canonical identity as
51
+ * the base, caller-supplied headers overriding individual keys. A caller that
52
+ * pins `User-Agent` takes that key; the other canonicals still apply.
53
+ *
54
+ * A null/undefined `callerHeaders` returns the canonical set alone (upstream
55
+ * `customHeaders ? {...} : defaultHeaders` shortcut).
56
+ */
57
+ export declare function mergeDashScopeTokenPlanHeaders(callerHeaders: Record<string, string> | undefined, version?: string): Record<string, string>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/ai",
4
- "version": "0.12.4",
4
+ "version": "0.12.5",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo and Gajae Code Contributors",
@@ -40,7 +40,7 @@
40
40
  "dependencies": {
41
41
  "@anthropic-ai/sdk": "^0.94.0",
42
42
  "@bufbuild/protobuf": "^2.12.0",
43
- "@gajae-code/utils": "0.12.4",
43
+ "@gajae-code/utils": "0.12.5",
44
44
  "openai": "^6.36.0",
45
45
  "partial-json": "^0.1.7",
46
46
  "zod": "4.4.3"
@@ -0,0 +1,84 @@
1
+ /**
2
+ * DashScope Token Plan canonical request headers.
3
+ *
4
+ * Reproduces QwenLM/qwen-code's DashScopeOpenAICompatibleProvider.buildHeaders()
5
+ * defaultHeaders so the built-in `alibaba-token-plan` provider emits the same
6
+ * client identity / cache / auth-type fingerprint upstream sends. DashScope is
7
+ * compatibility-sensitive to this fingerprint; a non-identical set can cause
8
+ * request instability and affect first-event latency (gajae-code #3557).
9
+ *
10
+ * Upstream pin (reproduce EXACTLY here):
11
+ * Repository: QwenLM/qwen-code
12
+ * Commit: f4cd6e1d8bbb1c24e7e5d1a40187d8e28aa7c4fb
13
+ * Version: 0.21.1
14
+ * Source: packages/core/src/core/openaiContentGenerator/provider/dashscope.ts
15
+ * buildHeaders():
16
+ * const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`;
17
+ * const defaultHeaders = {
18
+ * 'User-Agent': userAgent,
19
+ * 'X-DashScope-CacheControl': 'enable',
20
+ * 'X-DashScope-UserAgent': userAgent,
21
+ * 'X-DashScope-AuthType': authType,
22
+ * };
23
+ * return customHeaders ? { ...defaultHeaders, ...customHeaders } : defaultHeaders;
24
+ *
25
+ * The Token Plan preset authenticates with AuthType.USE_OPENAI ('openai'), so
26
+ * X-DashScope-AuthType is the constant 'openai'. Pin the version so an upstream
27
+ * bump is an explicit parity update rather than silent drift.
28
+ */
29
+ export const QWEN_CODE_UPSTREAM_REPO = "QwenLM/qwen-code";
30
+ export const QWEN_CODE_UPSTREAM_COMMIT = "f4cd6e1d8bbb1c24e7e5d1a40187d8e28aa7c4fb";
31
+ export const QWEN_CODE_UPSTREAM_VERSION = "0.21.1";
32
+
33
+ // Upstream Token Plan preset uses AuthType.USE_OPENAI = 'openai'.
34
+ const QWEN_CODE_TOKEN_PLAN_AUTH_TYPE = "openai";
35
+
36
+ /**
37
+ * The Qwen Code CLI version string used in identity headers. Pinned to the
38
+ * upstream version at {@link QWEN_CODE_UPSTREAM_COMMIT}; change both together
39
+ * as an explicit parity update.
40
+ */
41
+ export function qwenCodeUserAgent(version: string = QWEN_CODE_UPSTREAM_VERSION): string {
42
+ // process.platform / process.arch are read verbatim, matching upstream
43
+ // (e.g. "linux", "darwin", "win32"; "x64", "arm64"). No normalization.
44
+ return `QwenCode/${version} (${process.platform}; ${process.arch})`;
45
+ }
46
+
47
+ /**
48
+ * Canonical DashScope Token Plan headers (upstream defaultHeaders, no caller
49
+ * overrides applied). Exposed for tests/fixtures so the pinned wire set lives
50
+ * in exactly one place.
51
+ */
52
+ export function dashscopeTokenPlanDefaultHeaders(
53
+ version: string = QWEN_CODE_UPSTREAM_VERSION,
54
+ ): Readonly<Record<string, string>> {
55
+ const userAgent = qwenCodeUserAgent(version);
56
+ return Object.freeze({
57
+ "User-Agent": userAgent,
58
+ "X-DashScope-CacheControl": "enable",
59
+ "X-DashScope-UserAgent": userAgent,
60
+ "X-DashScope-AuthType": QWEN_CODE_TOKEN_PLAN_AUTH_TYPE,
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Merge canonical DashScope Token Plan identity headers onto a caller's header
66
+ * map, reproducing upstream buildHeaders() precedence EXACTLY:
67
+ * `{ ...defaultHeaders, ...customHeaders }` — caller wins per header.
68
+ *
69
+ * This mirrors GJC's existing kimi-code injection order
70
+ * (`headers = { ...getKimiCommonHeaders(), ...headers }`): canonical identity as
71
+ * the base, caller-supplied headers overriding individual keys. A caller that
72
+ * pins `User-Agent` takes that key; the other canonicals still apply.
73
+ *
74
+ * A null/undefined `callerHeaders` returns the canonical set alone (upstream
75
+ * `customHeaders ? {...} : defaultHeaders` shortcut).
76
+ */
77
+ export function mergeDashScopeTokenPlanHeaders(
78
+ callerHeaders: Record<string, string> | undefined,
79
+ version: string = QWEN_CODE_UPSTREAM_VERSION,
80
+ ): Record<string, string> {
81
+ const defaults = dashscopeTokenPlanDefaultHeaders(version);
82
+ if (!callerHeaders) return { ...defaults };
83
+ return { ...defaults, ...callerHeaders };
84
+ }
@@ -43,6 +43,7 @@ import {
43
43
  createOpenAIResponsesHistoryPayload,
44
44
  getOpenAIResponsesHistoryItems,
45
45
  getOpenAIResponsesHistoryPayload,
46
+ neutralizeReservedControlTokens,
46
47
  neutralizeResponsesInputControlTokens,
47
48
  normalizeSystemPrompts,
48
49
  sanitizeOpenAIResponsesHistoryItemsForReplay,
@@ -746,7 +747,12 @@ async function buildTransformedCodexRequestBody(
746
747
  }
747
748
  }
748
749
 
749
- const systemPrompts = normalizeSystemPrompts(context.systemPrompt);
750
+ // Neutralize leaked Harmony control tokens in the system prompt too:
751
+ // `params.instructions` and the developer messages prepended inside
752
+ // `transformRequestBody` bypass the `input` sanitizer above, so a poisoned
753
+ // system prompt rejects every turn with
754
+ // `Request blocked (code=invalid_prompt)`.
755
+ const systemPrompts = normalizeSystemPrompts(context.systemPrompt).map(neutralizeReservedControlTokens);
750
756
  if (systemPrompts.length > 0) {
751
757
  params.instructions = systemPrompts[0];
752
758
  }
@@ -68,6 +68,7 @@ import {
68
68
  resolveToolChoice,
69
69
  } from "../utils/tool-choice-capability";
70
70
  import { COMPOSER_EDIT_DISCIPLINE_PROMPT, isComposerHarnessModel } from "./composer-discipline";
71
+ import { mergeDashScopeTokenPlanHeaders } from "./dashscope-token-plan-headers";
71
72
  import {
72
73
  buildCopilotDynamicHeaders,
73
74
  hasCopilotVisionInput,
@@ -1071,6 +1072,14 @@ async function createClient(
1071
1072
  if (model.provider === "kimi-code") {
1072
1073
  headers = { ...getKimiCommonHeaders(), ...headers };
1073
1074
  }
1075
+ if (model.provider === "alibaba-token-plan") {
1076
+ // Emit Qwen Code's canonical DashScope request fingerprint (User-Agent /
1077
+ // X-DashScope-CacheControl / X-DashScope-UserAgent / X-DashScope-AuthType)
1078
+ // so DashScope treats the caller identically to upstream QwenLM/qwen-code.
1079
+ // Canonical identity is the base; caller headers win per key (upstream
1080
+ // `{...default, ...customHeaders}`). #3557.
1081
+ headers = mergeDashScopeTokenPlanHeaders(headers);
1082
+ }
1074
1083
  headers = applyOpenAIRequestTransformHeaders(headers, model.requestTransform, `Gajae-Code/${packageJson.version}`);
1075
1084
  let copilotPremiumRequests: number | undefined;
1076
1085
 
@@ -27,6 +27,7 @@ import {
27
27
  getOpenAIResponsesHistoryItems,
28
28
  getOpenAIResponsesHistoryPayload,
29
29
  isInvalidPromptError,
30
+ neutralizeReservedControlTokens,
30
31
  neutralizeResponsesInputControlTokens,
31
32
  normalizeSystemPrompts,
32
33
  resolveCacheRetention,
@@ -60,6 +61,7 @@ import {
60
61
  resolveToolChoice,
61
62
  } from "../utils/tool-choice-capability";
62
63
  import { COMPOSER_EDIT_DISCIPLINE_PROMPT, isComposerHarnessModel } from "./composer-discipline";
64
+ import { mergeDashScopeTokenPlanHeaders } from "./dashscope-token-plan-headers";
63
65
  import {
64
66
  buildCopilotDynamicHeaders,
65
67
  hasCopilotVisionInput,
@@ -446,8 +448,17 @@ function createClient(
446
448
  }
447
449
  const rawApiKey = apiKey;
448
450
 
451
+ const baseHeaders =
452
+ model.provider === "alibaba-token-plan"
453
+ ? // Emit Qwen Code's canonical DashScope request fingerprint (User-Agent /
454
+ // X-DashScope-CacheControl / X-DashScope-UserAgent / X-DashScope-AuthType)
455
+ // so DashScope treats the caller identically to upstream QwenLM/qwen-code.
456
+ // Canonical identity is the base; caller headers win per key (upstream
457
+ // `{...default, ...customHeaders}`). #3557.
458
+ mergeDashScopeTokenPlanHeaders({ ...(model.headers ?? {}), ...(extraHeaders ?? {}) })
459
+ : { ...(model.headers ?? {}), ...(extraHeaders ?? {}) };
449
460
  const headers = applyOpenAIRequestTransformHeaders(
450
- { ...(model.headers ?? {}), ...(extraHeaders ?? {}) },
461
+ baseHeaders,
451
462
  model.requestTransform,
452
463
  `Gajae-Code/${packageJson.version}`,
453
464
  );
@@ -523,7 +534,13 @@ function buildParams(
523
534
  );
524
535
  const messages: ResponseInput = neutralizeResponsesInputControlTokens(conversationMessages);
525
536
 
526
- const systemPrompts = normalizeSystemPrompts(context.systemPrompt);
537
+ // Neutralize leaked Harmony control tokens in the system prompt too: the
538
+ // `instructions` field and developer-role messages bypass the `input`
539
+ // request-boundary sanitizer above, and a poisoned system prompt (e.g.
540
+ // injected project context quoting `<|channel|>` markers) rejects EVERY
541
+ // turn with `Request blocked (code=invalid_prompt)` — unrepairable by the
542
+ // history circuit breaker.
543
+ const systemPrompts = normalizeSystemPrompts(context.systemPrompt).map(neutralizeReservedControlTokens);
527
544
  if (isComposerHarnessModel(model.id)) {
528
545
  systemPrompts.unshift(COMPOSER_EDIT_DISCIPLINE_PROMPT);
529
546
  }