11import {
22 defineCommand ,
33 chatPath ,
4+ responsesPath ,
45 parseSSE ,
56 detectOutputFormat ,
67 readTextFromPathOrStdin ,
78 type ChatMessage ,
89 type ChatRequest ,
910 type ChatResponse ,
11+ type ResponsesRequest ,
12+ type ResponsesResponse ,
13+ type ResponsesStreamEvent ,
1014 type StreamChunk ,
1115 type FlagsDef ,
1216 type ParsedFlags ,
1317} from "bailian-cli-core" ;
1418import { ansi , emitResult , emitBare } from "bailian-cli-runtime" ;
1519import { readFileSync } from "fs" ;
20+ import {
21+ assertResponsesStreamCompleted ,
22+ inspectResponsesStreamEvent ,
23+ extractResponsesText ,
24+ } from "./responses.ts" ;
1625
1726const CHAT_FLAGS = {
27+ api : {
28+ type : "string" ,
29+ valueHint : "<chat|responses>" ,
30+ choices : [ "chat" , "responses" ] as const ,
31+ description : "API to call (default: chat)" ,
32+ } ,
1833 model : { type : "string" , valueHint : "<model>" , description : "Model ID (default: qwen3.8-max)" } ,
1934 message : {
2035 type : "array" ,
@@ -72,31 +87,31 @@ function parseMessages(flags: ChatFlags): ParsedMessages {
7287 if ( flags . messagesFile ) {
7388 const raw = readTextFromPathOrStdin ( flags . messagesFile ) ;
7489 const parsed = JSON . parse ( raw ) as Array < { role : string ; content : string } > ;
75- for ( const m of parsed ) {
76- if ( m . role === "system" ) {
77- system = typeof m . content === "string" ? m . content : "" ;
90+ for ( const parsedMessage of parsed ) {
91+ if ( parsedMessage . role === "system" ) {
92+ system = typeof parsedMessage . content === "string" ? parsedMessage . content : "" ;
7893 } else {
79- messages . push ( m as ChatMessage ) ;
94+ messages . push ( parsedMessage as ChatMessage ) ;
8095 }
8196 }
8297 }
8398
8499 if ( flags . message ) {
85100 const validRoles = new Set ( [ "system" , "user" , "assistant" ] ) ;
86- const msgs = flags . message ;
87- for ( const m of msgs ) {
88- const colonIdx = m . indexOf ( ":" ) ;
89- const maybeRole = colonIdx !== - 1 ? m . slice ( 0 , colonIdx ) : "" ;
101+ const messageValues = flags . message ;
102+ for ( const messageValue of messageValues ) {
103+ const colonIndex = messageValue . indexOf ( ":" ) ;
104+ const maybeRole = colonIndex !== - 1 ? messageValue . slice ( 0 , colonIndex ) : "" ;
90105
91106 if ( validRoles . has ( maybeRole ) ) {
92- const content = m . slice ( colonIdx + 1 ) ;
107+ const content = messageValue . slice ( colonIndex + 1 ) ;
93108 if ( maybeRole === "system" ) {
94109 system = content ;
95110 } else {
96111 messages . push ( { role : maybeRole as "user" | "assistant" , content } ) ;
97112 }
98113 } else {
99- messages . push ( { role : "user" , content : m } ) ;
114+ messages . push ( { role : "user" , content : messageValue } ) ;
100115 }
101116 }
102117 }
@@ -105,24 +120,33 @@ function parseMessages(flags: ChatFlags): ParsedMessages {
105120}
106121
107122export default defineCommand ( {
108- description : "Send a chat completion (OpenAI compatible, DashScope)" ,
123+ description : "Send a text model request (OpenAI compatible, DashScope)" ,
109124 auth : "apiKey" ,
110125 usageArgs : "--message <text> [flags]" ,
111126 flags : CHAT_FLAGS ,
112127 exampleArgs : [
113128 '--message "What is Qwen?"' ,
129+ `--api responses --model qwen3.8-max --tool '{"type":"web_search"}' --message "Search for recent Alibaba Cloud news"` ,
114130 '--model qwen-max --system "You are a coding assistant." --message "Write fizzbuzz in Python"' ,
115131 '--message "Hello" --message "assistant:Hi!" --message "How are you?"' ,
116132 "--messages-file - --stream" ,
117133 '--message "Hello" --output json' ,
118134 '--model qwq-plus --message "Solve 1+1" --enable-thinking' ,
119135 ] ,
120- validate : ( f ) =>
121- ! f . message && ! f . messagesFile ? "Provide --message or --messages-file." : undefined ,
136+ validate : ( flags ) => {
137+ if ( ! flags . message && ! flags . messagesFile ) {
138+ return "Provide --message or --messages-file." ;
139+ }
140+ if ( flags . api === "responses" && flags . thinkingBudget !== undefined ) {
141+ return "--thinking-budget is not supported by the Responses API." ;
142+ }
143+ return undefined ;
144+ } ,
122145 async run ( ctx ) {
123146 const { settings, flags } = ctx ;
124147 const { system, messages } = parseMessages ( flags ) ;
125148
149+ const api = flags . api ?? "chat" ;
126150 const model = flags . model || settings . defaultTextModel || "qwen3.8-max" ;
127151 const shouldStream = flags . stream || process . stdout . isTTY ;
128152 const format = detectOutputFormat ( settings . output ) ;
@@ -134,29 +158,39 @@ export default defineCommand({
134158 }
135159 allMessages . push ( ...messages ) ;
136160
137- const body : ChatRequest = {
138- model,
139- messages : allMessages ,
140- max_tokens : flags . maxTokens ?? 4096 ,
141- stream : shouldStream ,
142- } ;
161+ let body : ChatRequest | ResponsesRequest ;
162+ if ( api === "responses" ) {
163+ body = {
164+ model,
165+ input : allMessages ,
166+ max_output_tokens : flags . maxTokens ?? 4096 ,
167+ stream : shouldStream ,
168+ } ;
169+ } else {
170+ body = {
171+ model,
172+ messages : allMessages ,
173+ max_tokens : flags . maxTokens ?? 4096 ,
174+ stream : shouldStream ,
175+ } ;
176+ }
143177
144178 if ( flags . temperature !== undefined ) body . temperature = flags . temperature ;
145179 if ( flags . topP !== undefined ) body . top_p = flags . topP ;
146180
147181 if ( flags . enableThinking ) {
148182 body . enable_thinking = true ;
149- if ( flags . thinkingBudget !== undefined ) {
183+ if ( api === "chat" && "messages" in body && flags . thinkingBudget !== undefined ) {
150184 body . thinking_budget = flags . thinkingBudget ;
151185 }
152186 }
153187
154188 if ( flags . tool ) {
155- const tools = flags . tool . map ( ( t ) => {
189+ const tools = flags . tool . map ( ( toolValue ) => {
156190 try {
157- return JSON . parse ( t ) ;
191+ return JSON . parse ( toolValue ) ;
158192 } catch {
159- const raw = readFileSync ( t , "utf-8" ) ;
193+ const raw = readFileSync ( toolValue , "utf-8" ) ;
160194 return JSON . parse ( raw ) ;
161195 }
162196 } ) ;
@@ -169,24 +203,45 @@ export default defineCommand({
169203 }
170204
171205 if ( shouldStream ) {
172- const res = await ctx . client . request ( {
173- path : chatPath ( ) ,
206+ const responseStream = await ctx . client . request ( {
207+ path : api === "responses" ? responsesPath ( ) : chatPath ( ) ,
174208 method : "POST" ,
175209 body,
176210 stream : true ,
177211 } ) ;
178212
179213 let textContent = "" ;
180214 let inThinking = false ;
215+ let responsesCompleted = false ;
181216 const writesStreamingStdout = format === "text" ;
182217 const isTTY = process . stdout . isTTY ;
183218 const statusOut =
184219 format === "json" ? process . stderr : isTTY ? process . stdout : process . stderr ;
185220 const resultOut = process . stdout ;
186221 const statusColor = ansi ( statusOut ) ;
187222
188- for await ( const event of parseSSE ( res ) ) {
223+ for await ( const event of parseSSE ( responseStream ) ) {
189224 if ( event . data === "[DONE]" ) break ;
225+ if ( api === "responses" ) {
226+ let parsedEvent : ResponsesStreamEvent ;
227+ try {
228+ parsedEvent = JSON . parse ( event . data ) as ResponsesStreamEvent ;
229+ } catch {
230+ continue ;
231+ }
232+
233+ const update = inspectResponsesStreamEvent ( parsedEvent ) ;
234+ if ( update . delta ) {
235+ textContent += update . delta ;
236+ if ( writesStreamingStdout ) resultOut . write ( update . delta ) ;
237+ }
238+ if ( update . completed ) {
239+ responsesCompleted = true ;
240+ break ;
241+ }
242+ continue ;
243+ }
244+
190245 try {
191246 const parsed = JSON . parse ( event . data ) as StreamChunk ;
192247
@@ -216,13 +271,28 @@ export default defineCommand({
216271 // Skip unparseable chunks
217272 }
218273 }
274+ if ( api === "responses" ) assertResponsesStreamCompleted ( responsesCompleted ) ;
219275 if ( inThinking ) statusOut . write ( statusColor . reset ) ;
220276
221277 if ( format === "json" ) {
222278 emitResult ( { content : textContent } , format ) ;
223279 } else {
224280 resultOut . write ( "\n" ) ;
225281 }
282+ } else if ( api === "responses" ) {
283+ const response = await ctx . client . requestJson < ResponsesResponse > ( {
284+ path : responsesPath ( ) ,
285+ method : "POST" ,
286+ body,
287+ } ) ;
288+
289+ const text = extractResponsesText ( response ) ;
290+
291+ if ( settings . quiet || format === "text" ) {
292+ emitBare ( text ) ;
293+ } else {
294+ emitResult ( response , format ) ;
295+ }
226296 } else {
227297 const response = await ctx . client . requestJson < ChatResponse > ( {
228298 path : chatPath ( ) ,
0 commit comments