From 9ba4a9d1a3649caa00c888dfd904d98b96a5af84 Mon Sep 17 00:00:00 2001 From: "lisheng.lisheng" Date: Sun, 16 Aug 2026 11:23:57 +0800 Subject: [PATCH] feat(dsh): add Responses-API route for TokenPlan Qwen models and enhance event text handling --- packages/dsh/cordis.patch.yml | 48 ++++++++++++++++++++ packages/dsh/src/tool-managed-agent/index.ts | 31 +++++++++++-- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/packages/dsh/cordis.patch.yml b/packages/dsh/cordis.patch.yml index 0ae0a51..4d515f8 100644 --- a/packages/dsh/cordis.patch.yml +++ b/packages/dsh/cordis.patch.yml @@ -69,6 +69,54 @@ compat: thinkingFormat: deepseek + # Responses-API route for the TokenPlan Qwen thinking models: same + # gateway and key as bailian-tokenplan, but OpenAI Responses protocol + # (`/responses`) instead of chat/completions. Reasoning arrives as + # native `reasoning` output items; the gateway's + # `response.reasoning_text.delta` SSE event is one pi-ai's + # openai-responses adapter explicitly consumes. The three models stay + # listed on the completions provider above, so both routes remain + # selectable; delete them there to make Responses the only route. + # + # No `thinkingFormat: qwen` here — that compat maps the completions + # `reasoning_content` field, pi-ai types it only for + # openai-completions, and resolution rejects such switches on this + # protocol. Reasoning effort instead goes through each model's + # `reasoningEfforts`, whose wire spellings land in the gateway's + # `reasoning.effort` request parameter. The four levels below were the + # only ones probed; xhigh/max were not offered to the gateway. + # + # Verified on 2026-08-15 per model against /responses: non-stream and + # stream bodies, one function call, and a colour question about a test + # PNG. qwen3.7-max rejects image content with HTTP 400, so it carries + # no `input: [text, image]`. + bailian-tokenplan-responses: + displayName: Aliyun Bailian TokenPlan (Responses) + api: openai-responses + baseURL: https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1 + apiKeyEnv: BAILIAN_TOKENPLAN_API_KEY + models: + - id: qwen3.8-max + input: [text, image] + reasoningEfforts: + minimal: minimal + low: low + medium: medium + high: high + - id: qwen3.7-plus + input: [text, image] + reasoningEfforts: + minimal: minimal + low: low + medium: medium + high: high + - id: qwen3.7-max + reasoningEfforts: + minimal: minimal + low: low + medium: medium + high: high + - insert: - id: bailian-tool-vision name: bailian-cli-dsh/tool-vision diff --git a/packages/dsh/src/tool-managed-agent/index.ts b/packages/dsh/src/tool-managed-agent/index.ts index 31bcbc6..931c65f 100644 --- a/packages/dsh/src/tool-managed-agent/index.ts +++ b/packages/dsh/src/tool-managed-agent/index.ts @@ -128,11 +128,36 @@ interface SessionRunResponse { events?: readonly { type?: string; content?: unknown; role?: string }[]; } -/** Assistant-visible text of a finished remote session. */ +/** + * Text of one sanitized envelope event. `bl --output json` emits the SDK's + * sanitized `SessionEvent` shape, where `content` is an ARRAY of content + * blocks (`[{ type: "text", text }]`) — never a plain string — and `type` is + * the provider's raw event type. Tolerate a legacy string `content` too. + */ +function eventText(content: unknown): string { + if (typeof content === "string") return content; + if (!Array.isArray(content)) return ""; + return content + .map((block) => + block !== null && + typeof block === "object" && + typeof (block as { text?: unknown }).text === "string" + ? (block as { text: string }).text + : "", + ) + .join(""); +} + +/** + * Assistant-visible text of a finished remote session. The envelope echoes + * the user prompt as `type: "message", role: "user"`, so keep only non-user + * message events and join their text blocks. + */ function assistantText(response: SessionRunResponse): string { return (response.events ?? []) - .filter((event) => event.type === "message" && typeof event.content === "string") - .map((event) => event.content as string) + .filter((event) => event.type === "message" && event.role !== "user") + .map((event) => eventText(event.content)) + .filter((text) => text.length > 0) .join("\n") .trim(); }