@ryanfw/prompt-orchestration-pipeline 1.3.2 → 1.3.4

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.
Files changed (206) hide show
  1. package/docs/http-api.md +80 -4
  2. package/package.json +1 -4
  3. package/src/api/__tests__/index.test.ts +443 -32
  4. package/src/api/index.ts +108 -127
  5. package/src/cli/__tests__/analyze-task.test.ts +40 -7
  6. package/src/cli/__tests__/index.test.ts +337 -17
  7. package/src/cli/__tests__/types.test.ts +0 -18
  8. package/src/cli/__tests__/update-pipeline-json.test.ts +19 -9
  9. package/src/cli/analyze-task.ts +3 -14
  10. package/src/cli/index.ts +73 -61
  11. package/src/cli/types.ts +0 -13
  12. package/src/cli/update-pipeline-json.ts +3 -14
  13. package/src/config/__tests__/paths.test.ts +46 -1
  14. package/src/config/__tests__/pipeline-registry.test.ts +767 -0
  15. package/src/config/__tests__/sse-events.test.ts +470 -0
  16. package/src/config/__tests__/statuses.test.ts +41 -0
  17. package/src/config/paths.ts +11 -2
  18. package/src/config/pipeline-registry.ts +258 -0
  19. package/src/config/sse-events.ts +122 -0
  20. package/src/config/statuses.ts +20 -1
  21. package/src/core/__tests__/agent-step.test.ts +115 -0
  22. package/src/core/__tests__/config.test.ts +545 -105
  23. package/src/core/__tests__/core-boot-resolution.test.ts +181 -0
  24. package/src/core/__tests__/job-concurrency.test.ts +260 -0
  25. package/src/core/__tests__/job-submission.test.ts +396 -0
  26. package/src/core/__tests__/job-view.test.ts +1341 -0
  27. package/src/core/__tests__/json-file.test.ts +111 -0
  28. package/src/core/__tests__/lifecycle-policy.test.ts +81 -7
  29. package/src/core/__tests__/logger.test.ts +22 -0
  30. package/src/core/__tests__/orchestrator-no-deprecated-exports.test.ts +41 -0
  31. package/src/core/__tests__/orchestrator.test.ts +615 -2
  32. package/src/core/__tests__/pipeline-runner.test.ts +946 -9
  33. package/src/core/__tests__/redact.test.ts +153 -0
  34. package/src/core/__tests__/runner-liveness.test.ts +910 -0
  35. package/src/core/__tests__/seed-naming.test.ts +57 -0
  36. package/src/core/__tests__/single-derivation.test.ts +159 -0
  37. package/src/core/__tests__/task-runner.test.ts +594 -3
  38. package/src/core/__tests__/task-telemetry.test.ts +241 -0
  39. package/src/core/__tests__/workspace-root-resolution-guard.test.ts +62 -0
  40. package/src/core/agent-step.ts +4 -1
  41. package/src/core/agent-types.ts +5 -4
  42. package/src/core/config.ts +134 -222
  43. package/src/core/control.ts +3 -0
  44. package/src/core/job-concurrency.ts +56 -20
  45. package/src/core/job-submission.ts +133 -0
  46. package/src/core/job-view.ts +473 -0
  47. package/src/core/json-file.ts +45 -0
  48. package/src/core/lifecycle-policy.ts +23 -8
  49. package/src/core/logger.ts +0 -29
  50. package/src/core/orchestrator.ts +200 -74
  51. package/src/core/pipeline-runner.ts +85 -53
  52. package/src/core/redact.ts +40 -0
  53. package/src/core/runner-liveness.ts +280 -0
  54. package/src/core/seed-naming.ts +9 -0
  55. package/src/core/status-writer.ts +27 -33
  56. package/src/core/task-runner.ts +356 -319
  57. package/src/core/task-telemetry.ts +107 -0
  58. package/src/harness/__tests__/subprocess.test.ts +112 -1
  59. package/src/harness/subprocess.ts +55 -16
  60. package/src/llm/__tests__/index.test.ts +684 -33
  61. package/src/llm/index.ts +310 -67
  62. package/src/providers/__tests__/alibaba.test.ts +67 -6
  63. package/src/providers/__tests__/anthropic.test.ts +35 -14
  64. package/src/providers/__tests__/base.test.ts +62 -0
  65. package/src/providers/__tests__/claude-code.test.ts +99 -14
  66. package/src/providers/__tests__/deepseek.test.ts +16 -6
  67. package/src/providers/__tests__/gemini.test.ts +47 -25
  68. package/src/providers/__tests__/moonshot.test.ts +27 -0
  69. package/src/providers/__tests__/openai.test.ts +262 -74
  70. package/src/providers/__tests__/opencode.test.ts +77 -0
  71. package/src/providers/__tests__/request-timeout-contract.test.ts +226 -0
  72. package/src/providers/__tests__/stream-accumulator.test.ts +52 -0
  73. package/src/providers/__tests__/types.test.ts +85 -0
  74. package/src/providers/__tests__/zero-fill-guard.test.ts +62 -0
  75. package/src/providers/__tests__/zhipu.test.ts +27 -14
  76. package/src/providers/alibaba.ts +20 -39
  77. package/src/providers/anthropic.ts +23 -11
  78. package/src/providers/base.ts +19 -0
  79. package/src/providers/claude-code.ts +27 -18
  80. package/src/providers/deepseek.ts +9 -28
  81. package/src/providers/gemini.ts +20 -58
  82. package/src/providers/moonshot.ts +15 -11
  83. package/src/providers/openai.ts +79 -61
  84. package/src/providers/opencode.ts +16 -33
  85. package/src/providers/stream-accumulator.ts +27 -21
  86. package/src/providers/types.ts +29 -4
  87. package/src/providers/zhipu.ts +15 -44
  88. package/src/task-analysis/__tests__/analyzer.test.ts +0 -0
  89. package/src/task-analysis/__tests__/enrichers-schema-deducer.test.ts +34 -2
  90. package/src/task-analysis/__tests__/no-ast-imports.test.ts +52 -0
  91. package/src/task-analysis/__tests__/repository.test.ts +65 -0
  92. package/src/task-analysis/__tests__/types.test.ts +63 -130
  93. package/src/task-analysis/analyzer.ts +91 -0
  94. package/src/task-analysis/enrichers/schema-deducer.ts +13 -2
  95. package/src/task-analysis/index.ts +2 -36
  96. package/src/task-analysis/repository.ts +45 -0
  97. package/src/task-analysis/types.ts +42 -58
  98. package/src/ui/client/__tests__/api.test.ts +143 -1
  99. package/src/ui/client/__tests__/bootstrap.test.ts +178 -62
  100. package/src/ui/client/__tests__/job-adapter.test.ts +203 -2
  101. package/src/ui/client/__tests__/load-state.test.ts +70 -0
  102. package/src/ui/client/__tests__/types.test.ts +66 -3
  103. package/src/ui/client/__tests__/useJobDetailWithUpdates.test.ts +390 -77
  104. package/src/ui/client/__tests__/useJobList.test.ts +198 -23
  105. package/src/ui/client/__tests__/useJobListWithUpdates.test.ts +218 -7
  106. package/src/ui/client/adapters/__tests__/job-adapter.test.ts +186 -0
  107. package/src/ui/client/adapters/job-adapter.ts +41 -16
  108. package/src/ui/client/api.ts +38 -15
  109. package/src/ui/client/bootstrap.ts +19 -14
  110. package/src/ui/client/hooks/useJobDetailWithUpdates.ts +73 -97
  111. package/src/ui/client/hooks/useJobList.ts +26 -31
  112. package/src/ui/client/hooks/useJobListWithUpdates.ts +42 -76
  113. package/src/ui/client/load-state.ts +20 -0
  114. package/src/ui/client/reducers/__tests__/job-events.test.ts +568 -0
  115. package/src/ui/client/reducers/job-events.ts +137 -0
  116. package/src/ui/client/types.ts +16 -20
  117. package/src/ui/components/DAGGrid.tsx +6 -1
  118. package/src/ui/components/JobDetail.tsx +12 -4
  119. package/src/ui/components/JobTable.tsx +41 -13
  120. package/src/ui/components/StageTimeline.tsx +8 -20
  121. package/src/ui/components/TaskAnalysisDisplay.tsx +48 -6
  122. package/src/ui/components/__tests__/DAGGrid.test.tsx +36 -0
  123. package/src/ui/components/__tests__/JobDetail.test.tsx +112 -0
  124. package/src/ui/components/__tests__/JobTable.test.tsx +137 -1
  125. package/src/ui/components/__tests__/StageTimeline.test.tsx +5 -6
  126. package/src/ui/components/__tests__/TaskAnalysisDisplay.test.tsx +64 -0
  127. package/src/ui/components/types.ts +35 -15
  128. package/src/ui/dist/assets/{index--RH3sAt3.js → index-DN3-zvtP.js} +4324 -263
  129. package/src/ui/dist/assets/index-DN3-zvtP.js.map +1 -0
  130. package/src/ui/dist/assets/style-CtZBnjlR.css +2 -0
  131. package/src/ui/dist/index.html +2 -2
  132. package/src/ui/pages/PipelineDetail.tsx +60 -4
  133. package/src/ui/pages/PromptPipelineDashboard.tsx +59 -7
  134. package/src/ui/pages/__tests__/PromptPipelineDashboard.test.tsx +114 -32
  135. package/src/ui/pages/__tests__/pages.test.tsx +236 -42
  136. package/src/ui/server/__tests__/concurrency-endpoint.test.ts +54 -0
  137. package/src/ui/server/__tests__/config-bridge-node.test.ts +34 -1
  138. package/src/ui/server/__tests__/config-bridge.test.ts +9 -1
  139. package/src/ui/server/__tests__/embedded-assets.test.ts +66 -0
  140. package/src/ui/server/__tests__/file-endpoints.test.ts +183 -5
  141. package/src/ui/server/__tests__/gate-endpoints.test.ts +55 -0
  142. package/src/ui/server/__tests__/index.test.ts +63 -0
  143. package/src/ui/server/__tests__/job-control-endpoints.test.ts +512 -2
  144. package/src/ui/server/__tests__/job-endpoints.test.ts +158 -2
  145. package/src/ui/server/__tests__/read-static-path-guard.test.ts +94 -0
  146. package/src/ui/server/__tests__/router-root-isolation.test.ts +204 -0
  147. package/src/ui/server/__tests__/router-threading.test.ts +148 -0
  148. package/src/ui/server/__tests__/router.test.ts +104 -0
  149. package/src/ui/server/__tests__/sse-broadcast.test.ts +44 -0
  150. package/src/ui/server/__tests__/sse-enhancer.test.ts +70 -0
  151. package/src/ui/server/config-bridge-node.ts +8 -26
  152. package/src/ui/server/config-bridge.ts +3 -4
  153. package/src/ui/server/embedded-assets-imports.d.ts +44 -0
  154. package/src/ui/server/embedded-assets.ts +13 -2
  155. package/src/ui/server/endpoints/__tests__/meta-endpoint.test.ts +1 -0
  156. package/src/ui/server/endpoints/__tests__/pipeline-analysis-endpoint.test.ts +167 -0
  157. package/src/ui/server/endpoints/__tests__/schema-file-endpoint.test.ts +104 -0
  158. package/src/ui/server/endpoints/__tests__/task-analysis-endpoint.test.ts +103 -0
  159. package/src/ui/server/endpoints/__tests__/upload-endpoints.test.ts +162 -96
  160. package/src/ui/server/endpoints/concurrency-endpoint.ts +2 -1
  161. package/src/ui/server/endpoints/create-pipeline-endpoint.ts +14 -29
  162. package/src/ui/server/endpoints/file-endpoints.ts +15 -10
  163. package/src/ui/server/endpoints/gate-endpoints.ts +2 -6
  164. package/src/ui/server/endpoints/job-control-endpoints.ts +129 -141
  165. package/src/ui/server/endpoints/job-endpoints.ts +7 -0
  166. package/src/ui/server/endpoints/meta-endpoint.ts +1 -1
  167. package/src/ui/server/endpoints/pipeline-analysis-endpoint.ts +99 -11
  168. package/src/ui/server/endpoints/schema-file-endpoint.ts +11 -3
  169. package/src/ui/server/endpoints/task-analysis-endpoint.ts +21 -5
  170. package/src/ui/server/endpoints/task-save-endpoint.ts +1 -2
  171. package/src/ui/server/endpoints/upload-endpoints.ts +5 -40
  172. package/src/ui/server/index.ts +19 -14
  173. package/src/ui/server/job-reader.ts +14 -2
  174. package/src/ui/server/router.ts +33 -10
  175. package/src/ui/server/sse-broadcast.ts +14 -3
  176. package/src/ui/server/sse-enhancer.ts +12 -2
  177. package/src/ui/state/__tests__/schema-loader.test.ts +11 -1
  178. package/src/ui/state/__tests__/snapshot.test.ts +120 -14
  179. package/src/ui/state/__tests__/types.test.ts +104 -5
  180. package/src/ui/state/snapshot.ts +2 -2
  181. package/src/ui/state/transformers/__tests__/list-transformer.test.ts +85 -2
  182. package/src/ui/state/transformers/__tests__/status-transformer.test.ts +250 -22
  183. package/src/ui/state/transformers/list-transformer.ts +13 -3
  184. package/src/ui/state/transformers/status-transformer.ts +36 -170
  185. package/src/ui/state/types.ts +15 -48
  186. package/src/utils/__tests__/path-containment.test.ts +160 -0
  187. package/src/{ui/server/utils → utils}/path-containment.ts +21 -0
  188. package/src/task-analysis/__tests__/enrichers-analysis-writer.test.ts +0 -86
  189. package/src/task-analysis/__tests__/enrichers-artifact-resolver.test.ts +0 -124
  190. package/src/task-analysis/__tests__/extractors-artifacts.test.ts +0 -133
  191. package/src/task-analysis/__tests__/extractors-llm-calls.test.ts +0 -46
  192. package/src/task-analysis/__tests__/extractors-stages.test.ts +0 -52
  193. package/src/task-analysis/__tests__/index.test.ts +0 -143
  194. package/src/task-analysis/__tests__/parser.test.ts +0 -41
  195. package/src/task-analysis/__tests__/utils-ast.test.ts +0 -82
  196. package/src/task-analysis/enrichers/analysis-writer.ts +0 -75
  197. package/src/task-analysis/enrichers/artifact-resolver.ts +0 -89
  198. package/src/task-analysis/extractors/artifacts.ts +0 -143
  199. package/src/task-analysis/extractors/llm-calls.ts +0 -117
  200. package/src/task-analysis/extractors/stages.ts +0 -45
  201. package/src/task-analysis/parser.ts +0 -20
  202. package/src/task-analysis/utils/ast.ts +0 -45
  203. package/src/ui/dist/assets/index--RH3sAt3.js.map +0 -1
  204. package/src/ui/dist/assets/style-CSSKuMOe.css +0 -2
  205. package/src/ui/embedded-assets.js +0 -12
  206. package/src/ui/server/__tests__/path-containment.test.ts +0 -54
@@ -10,6 +10,8 @@ import {
10
10
  stripMarkdownFences,
11
11
  tryParseJSON,
12
12
  ensureJsonResponseFormat,
13
+ isJsonResponseFormat,
14
+ extractResponseSchema,
13
15
  } from "./base.ts";
14
16
  import { IdleTimeoutController } from "./stream-accumulator.ts";
15
17
  import { ProviderJsonParseError } from "./types.ts";
@@ -18,7 +20,6 @@ import type { Response as OAIResponse } from "openai/resources/responses/respons
18
20
  import type { ChatCompletion } from "openai/resources/chat/completions/completions.mjs";
19
21
 
20
22
  const DEFAULT_MODEL = "gpt-5-chat-latest";
21
- const DEFAULT_RESPONSE_FORMAT = "json_object";
22
23
  const DEFAULT_MAX_RETRIES = 3;
23
24
  const GPT5_PATTERN = /^gpt-5/i;
24
25
 
@@ -114,15 +115,14 @@ async function callResponsesAPI(
114
115
  const stripped = stripMarkdownFences(rawText);
115
116
  const parsed = tryParseJSON(stripped);
116
117
 
117
- // Estimate usage at ~4 chars/token since Responses API may not return usage
118
- const inputTokens = response.usage?.input_tokens ?? Math.ceil(userMsg.length / 4);
119
- const outputTokens = response.usage?.output_tokens ?? Math.ceil(rawText.length / 4);
120
-
121
- const usage = {
122
- prompt_tokens: inputTokens,
123
- completion_tokens: outputTokens,
124
- total_tokens: inputTokens + outputTokens,
125
- };
118
+ const u = response.usage;
119
+ const usage: AdapterUsage | undefined = u
120
+ ? {
121
+ prompt_tokens: typeof u.input_tokens === "number" ? u.input_tokens : null,
122
+ completion_tokens: typeof u.output_tokens === "number" ? u.output_tokens : null,
123
+ total_tokens: typeof u.total_tokens === "number" ? u.total_tokens : null,
124
+ }
125
+ : undefined;
126
126
 
127
127
  return {
128
128
  content: typeof parsed === "string" ? parsed : (parsed as Record<string, unknown>),
@@ -161,7 +161,12 @@ async function callChatCompletionsAPI(
161
161
  max_tokens: maxTokens,
162
162
  };
163
163
 
164
- if (responseSchema != null || responseFormat === "json_object" || responseFormat === "json") {
164
+ if (responseSchema != null) {
165
+ params["response_format"] = {
166
+ type: "json_schema",
167
+ json_schema: { name: "Response", schema: responseSchema },
168
+ };
169
+ } else if (responseFormat === "json_object" || responseFormat === "json") {
165
170
  params["response_format"] = { type: "json_object" };
166
171
  }
167
172
 
@@ -180,11 +185,14 @@ async function callChatCompletionsAPI(
180
185
  const stripped = stripMarkdownFences(rawText);
181
186
  const parsed = tryParseJSON(stripped);
182
187
 
183
- const usage = {
184
- prompt_tokens: completion.usage?.prompt_tokens ?? 0,
185
- completion_tokens: completion.usage?.completion_tokens ?? 0,
186
- total_tokens: completion.usage?.total_tokens ?? 0,
187
- };
188
+ const u = completion.usage;
189
+ const usage: AdapterUsage | undefined = u
190
+ ? {
191
+ prompt_tokens: typeof u.prompt_tokens === "number" ? u.prompt_tokens : null,
192
+ completion_tokens: typeof u.completion_tokens === "number" ? u.completion_tokens : null,
193
+ total_tokens: typeof u.total_tokens === "number" ? u.total_tokens : null,
194
+ }
195
+ : undefined;
188
196
 
189
197
  return {
190
198
  content: typeof parsed === "string" ? parsed : (parsed as Record<string, unknown>),
@@ -203,6 +211,7 @@ async function callStreamingResponsesAPI(
203
211
  maxTokens: number,
204
212
  responseFormat: string,
205
213
  responseSchema: unknown | undefined,
214
+ timeoutMs: number,
206
215
  ): Promise<AdapterResponse> {
207
216
  const params: Record<string, unknown> = {
208
217
  model,
@@ -228,7 +237,7 @@ async function callStreamingResponsesAPI(
228
237
  params["text"] = { format: { type: "json_object" } };
229
238
  }
230
239
 
231
- const idle = new IdleTimeoutController(DEFAULT_REQUEST_TIMEOUT_MS);
240
+ const idle = new IdleTimeoutController(timeoutMs);
232
241
 
233
242
  const stream = await openai.responses.create({
234
243
  ...(params as Parameters<typeof openai.responses.create>[0]),
@@ -247,12 +256,19 @@ async function callStreamingResponsesAPI(
247
256
  text += (event as { delta?: string }).delta ?? "";
248
257
  } else if (type === "response.completed") {
249
258
  const resp = event.response as Record<string, unknown> | undefined;
250
- const u = resp?.usage as Record<string, number> | undefined;
251
- if (u) {
259
+ const u = resp?.usage as
260
+ | { input_tokens?: number; output_tokens?: number; total_tokens?: number }
261
+ | undefined;
262
+ if (
263
+ u &&
264
+ (typeof u.input_tokens === "number" ||
265
+ typeof u.output_tokens === "number" ||
266
+ typeof u.total_tokens === "number")
267
+ ) {
252
268
  usage = {
253
- prompt_tokens: u.input_tokens ?? 0,
254
- completion_tokens: u.output_tokens ?? 0,
255
- total_tokens: (u.input_tokens ?? 0) + (u.output_tokens ?? 0),
269
+ prompt_tokens: typeof u.input_tokens === "number" ? u.input_tokens : null,
270
+ completion_tokens: typeof u.output_tokens === "number" ? u.output_tokens : null,
271
+ total_tokens: typeof u.total_tokens === "number" ? u.total_tokens : null,
256
272
  };
257
273
  }
258
274
  }
@@ -261,16 +277,6 @@ async function callStreamingResponsesAPI(
261
277
  idle.cleanup();
262
278
  }
263
279
 
264
- if (!usage) {
265
- const inputTokens = Math.ceil(userMsg.length / 4);
266
- const outputTokens = Math.ceil(text.length / 4);
267
- usage = {
268
- prompt_tokens: inputTokens,
269
- completion_tokens: outputTokens,
270
- total_tokens: inputTokens + outputTokens,
271
- };
272
- }
273
-
274
280
  const stripped = stripMarkdownFences(text);
275
281
  const parsed = tryParseJSON(stripped);
276
282
 
@@ -295,6 +301,7 @@ async function callStreamingChatCompletionsAPI(
295
301
  presencePenalty: number | undefined,
296
302
  topP: number | undefined,
297
303
  stop: string | string[] | undefined,
304
+ timeoutMs: number,
298
305
  ): Promise<AdapterResponse> {
299
306
  const messages: Array<{ role: "system" | "user"; content: string }> = [];
300
307
 
@@ -312,7 +319,12 @@ async function callStreamingChatCompletionsAPI(
312
319
  stream_options: { include_usage: true },
313
320
  };
314
321
 
315
- if (responseSchema != null || responseFormat === "json_object" || responseFormat === "json") {
322
+ if (responseSchema != null) {
323
+ params["response_format"] = {
324
+ type: "json_schema",
325
+ json_schema: { name: "Response", schema: responseSchema },
326
+ };
327
+ } else if (responseFormat === "json_object" || responseFormat === "json") {
316
328
  params["response_format"] = { type: "json_object" };
317
329
  }
318
330
 
@@ -322,7 +334,7 @@ async function callStreamingChatCompletionsAPI(
322
334
  if (topP !== undefined) params["top_p"] = topP;
323
335
  if (stop !== undefined) params["stop"] = stop;
324
336
 
325
- const idle = new IdleTimeoutController(DEFAULT_REQUEST_TIMEOUT_MS);
337
+ const idle = new IdleTimeoutController(timeoutMs);
326
338
 
327
339
  const stream = await openai.chat.completions.create({
328
340
  ...(params as unknown as Parameters<typeof openai.chat.completions.create>[0]),
@@ -341,12 +353,20 @@ async function callStreamingChatCompletionsAPI(
341
353
  | undefined;
342
354
  text += choices?.[0]?.delta?.content ?? "";
343
355
 
344
- const u = chunk.usage as Record<string, number> | undefined;
345
- if (u && typeof u.prompt_tokens === "number") {
356
+ const u = chunk.usage as
357
+ | { prompt_tokens?: number; completion_tokens?: number; total_tokens?: number }
358
+ | undefined;
359
+ if (
360
+ u &&
361
+ (typeof u.prompt_tokens === "number" ||
362
+ typeof u.completion_tokens === "number" ||
363
+ typeof u.total_tokens === "number")
364
+ ) {
346
365
  usage = {
347
- prompt_tokens: u.prompt_tokens,
348
- completion_tokens: u.completion_tokens ?? 0,
349
- total_tokens: u.total_tokens ?? (u.prompt_tokens + (u.completion_tokens ?? 0)),
366
+ prompt_tokens: typeof u.prompt_tokens === "number" ? u.prompt_tokens : null,
367
+ completion_tokens:
368
+ typeof u.completion_tokens === "number" ? u.completion_tokens : null,
369
+ total_tokens: typeof u.total_tokens === "number" ? u.total_tokens : null,
350
370
  };
351
371
  }
352
372
  }
@@ -354,10 +374,6 @@ async function callStreamingChatCompletionsAPI(
354
374
  idle.cleanup();
355
375
  }
356
376
 
357
- if (!usage) {
358
- usage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
359
- }
360
-
361
377
  const stripped = stripMarkdownFences(text);
362
378
  const parsed = tryParseJSON(stripped);
363
379
 
@@ -376,7 +392,7 @@ export async function openaiChat(
376
392
  model = DEFAULT_MODEL,
377
393
  temperature = 0.7,
378
394
  maxTokens = 4096,
379
- responseFormat = DEFAULT_RESPONSE_FORMAT,
395
+ responseFormat,
380
396
  maxRetries = DEFAULT_MAX_RETRIES,
381
397
  seed,
382
398
  frequencyPenalty,
@@ -390,18 +406,16 @@ export async function openaiChat(
390
406
  ...rest
391
407
  } = options;
392
408
 
393
- ensureJsonResponseFormat(responseFormat, "openai");
409
+ const jsonMode = isJsonResponseFormat(responseFormat);
410
+ if (jsonMode) {
411
+ ensureJsonResponseFormat(responseFormat, "openai");
412
+ }
394
413
 
395
414
  const formatStr =
396
415
  typeof responseFormat === "string"
397
416
  ? responseFormat
398
- : (responseFormat as { type?: string }).type ?? "json_object";
399
- const responseSchema =
400
- typeof responseFormat === "object" &&
401
- responseFormat !== null &&
402
- "json_schema" in responseFormat
403
- ? (responseFormat as { json_schema?: unknown }).json_schema
404
- : undefined;
417
+ : (responseFormat as { type?: string })?.type ?? "";
418
+ const responseSchema = extractResponseSchema(responseFormat);
405
419
 
406
420
  const { systemMsg, userMsg } = extractMessages(messages);
407
421
  const timeoutMs = requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
@@ -415,18 +429,18 @@ export async function openaiChat(
415
429
  if (useResponsesAPI) {
416
430
  try {
417
431
  const result = await callStreamingResponsesAPI(
418
- openai, model, systemMsg, userMsg, temperature, maxTokens, formatStr, responseSchema,
432
+ openai, model, systemMsg, userMsg, temperature, maxTokens, formatStr, responseSchema, timeoutMs,
419
433
  );
420
- checkJsonParse(result, model, formatStr);
434
+ checkJsonParse(result, model, jsonMode);
421
435
  return result;
422
436
  } catch (responsesErr) {
423
437
  // On "unsupported" error, fall back to streaming Chat Completions within same attempt
424
438
  if (isUnsupportedError(responsesErr)) {
425
439
  const result = await callStreamingChatCompletionsAPI(
426
440
  openai, model, systemMsg, userMsg, temperature, maxTokens, formatStr, responseSchema,
427
- seed, frequencyPenalty, presencePenalty, topP, stop,
441
+ seed, frequencyPenalty, presencePenalty, topP, stop, timeoutMs,
428
442
  );
429
- checkJsonParse(result, model, formatStr);
443
+ checkJsonParse(result, model, jsonMode);
430
444
  return result;
431
445
  }
432
446
  throw responsesErr;
@@ -435,9 +449,9 @@ export async function openaiChat(
435
449
 
436
450
  const result = await callStreamingChatCompletionsAPI(
437
451
  openai, model, systemMsg, userMsg, temperature, maxTokens, formatStr, responseSchema,
438
- seed, frequencyPenalty, presencePenalty, topP, stop,
452
+ seed, frequencyPenalty, presencePenalty, topP, stop, timeoutMs,
439
453
  );
440
- checkJsonParse(result, model, formatStr);
454
+ checkJsonParse(result, model, jsonMode);
441
455
  return result;
442
456
  } catch (err) {
443
457
  lastError = err;
@@ -457,14 +471,18 @@ export async function openaiChat(
457
471
  function checkJsonParse(
458
472
  result: AdapterResponse,
459
473
  model: string,
460
- formatStr: string,
474
+ jsonMode: boolean,
461
475
  ): void {
462
- const isJsonMode = formatStr === "json" || formatStr === "json_object";
463
- if (isJsonMode && typeof result.content === "string") {
476
+ if (jsonMode && typeof result.content === "string") {
464
477
  throw new ProviderJsonParseError(
465
478
  "openai",
466
479
  model,
467
480
  (result.content as string).slice(0, 200),
481
+ undefined,
482
+ {
483
+ text: result.content as string,
484
+ usage: result.usage,
485
+ },
468
486
  );
469
487
  }
470
488
  }
@@ -5,6 +5,8 @@ import { createOpencodeClient } from "@opencode-ai/sdk/v2";
5
5
  import type { OpencodeClient } from "@opencode-ai/sdk/v2";
6
6
  import {
7
7
  DEFAULT_REQUEST_TIMEOUT_MS,
8
+ isJsonResponseFormat,
9
+ extractResponseSchema,
8
10
  isRetryableError,
9
11
  sleep,
10
12
  stripMarkdownFences,
@@ -23,6 +25,9 @@ import type {
23
25
  ResponseFormatObject,
24
26
  } from "./types.ts";
25
27
 
28
+ export const isJsonMode = isJsonResponseFormat;
29
+ export const jsonSchemaFromResponseFormat = extractResponseSchema;
30
+
26
31
  export type ParsedOpenCodeModel =
27
32
  | { providerID: string; modelID: string }
28
33
  | null;
@@ -65,35 +70,6 @@ export function buildOpenCodePromptText(messages: ChatMessage[]): string {
65
70
  .join("\n\n");
66
71
  }
67
72
 
68
- export function isJsonMode(
69
- responseFormat: string | ResponseFormatObject | undefined,
70
- ): boolean {
71
- if (responseFormat === undefined) return false;
72
-
73
- if (typeof responseFormat === "string") {
74
- return responseFormat === "json" || responseFormat === "json_object";
75
- }
76
-
77
- return (
78
- responseFormat.type === "json_object" ||
79
- responseFormat.json_schema != null
80
- );
81
- }
82
-
83
- export function jsonSchemaFromResponseFormat(
84
- responseFormat: string | ResponseFormatObject | undefined,
85
- ): unknown | undefined {
86
- if (
87
- responseFormat !== undefined &&
88
- typeof responseFormat === "object" &&
89
- responseFormat.json_schema != null
90
- ) {
91
- return responseFormat.json_schema;
92
- }
93
-
94
- return undefined;
95
- }
96
-
97
73
  export function defaultOpenCodePermission(): OpenCodePermissionConfig {
98
74
  return { "*": "deny" };
99
75
  }
@@ -288,11 +264,12 @@ function extractOpenCodeContent(
288
264
  raw: unknown,
289
265
  responseFormat: string | ResponseFormatObject | undefined,
290
266
  model: string,
267
+ context: { text?: string; usage?: AdapterUsage } = {},
291
268
  ): Record<string, unknown> | string {
292
269
  const structured = extractOpenCodeStructuredOutput(raw);
293
270
  if (structured != null) return structured;
294
271
 
295
- const text = extractOpenCodeText(raw);
272
+ const text = context.text ?? extractOpenCodeText(raw);
296
273
  const jsonMode = isJsonMode(responseFormat);
297
274
 
298
275
  if (jsonMode) {
@@ -307,6 +284,11 @@ function extractOpenCodeContent(
307
284
  "opencode",
308
285
  model || "default",
309
286
  cleaned.slice(0, 200),
287
+ undefined,
288
+ {
289
+ text,
290
+ usage: context.usage,
291
+ },
310
292
  );
311
293
  }
312
294
 
@@ -469,13 +451,14 @@ export async function opencodeChat(
469
451
  }
470
452
 
471
453
  const raw = result.data;
454
+ const text = extractOpenCodeText(raw);
455
+ const usage = normalizeOpenCodeUsage(raw);
472
456
  const content = extractOpenCodeContent(
473
457
  raw,
474
458
  responseFormat,
475
459
  modelString,
460
+ { text, usage },
476
461
  );
477
- const text = extractOpenCodeText(raw);
478
- const usage = normalizeOpenCodeUsage(raw);
479
462
 
480
463
  return { content, text, usage, raw };
481
464
  } catch (err) {
@@ -531,8 +514,8 @@ export async function opencodeChat(
531
514
 
532
515
  const cliResult = await runOpenCodeCli(args, env, requestTimeoutMs);
533
516
  const raw = { events: cliResult.events };
534
- const content = extractOpenCodeContent(raw, responseFormat, modelString);
535
517
  const text = extractOpenCodeText(raw);
518
+ const content = extractOpenCodeContent(raw, responseFormat, modelString, { text });
536
519
 
537
520
  return { content, text, raw };
538
521
  } catch (err) {
@@ -175,20 +175,20 @@ export async function* parseOpenAiSse(
175
175
  function parseOpenAiUsage(raw: unknown): AdapterUsage | undefined {
176
176
  if (!raw || typeof raw !== "object") return undefined;
177
177
  const u = raw as Record<string, unknown>;
178
- if (
179
- typeof u.prompt_tokens === "number" &&
180
- typeof u.completion_tokens === "number"
181
- ) {
182
- return {
183
- prompt_tokens: u.prompt_tokens,
184
- completion_tokens: u.completion_tokens,
185
- total_tokens:
186
- typeof u.total_tokens === "number"
187
- ? u.total_tokens
188
- : u.prompt_tokens + u.completion_tokens,
189
- };
178
+ const usage: AdapterUsage = {};
179
+ const prompt = u.prompt_tokens;
180
+ const completion = u.completion_tokens;
181
+ const total = u.total_tokens;
182
+
183
+ if (typeof prompt === "number") usage.prompt_tokens = prompt;
184
+ if (typeof completion === "number") usage.completion_tokens = completion;
185
+ if (typeof total === "number") {
186
+ usage.total_tokens = total;
187
+ } else if (typeof prompt === "number" && typeof completion === "number") {
188
+ usage.total_tokens = prompt + completion;
190
189
  }
191
- return undefined;
190
+
191
+ return Object.keys(usage).length > 0 ? usage : undefined;
192
192
  }
193
193
 
194
194
  // ─── Step 3: Anthropic event parser ──────────────────────────────────────────
@@ -291,14 +291,20 @@ export async function* parseGeminiSse(
291
291
  // Capture usage from usageMetadata (present on final chunk)
292
292
  const meta = parsed.usageMetadata as Record<string, unknown> | undefined;
293
293
  if (meta) {
294
- const prompt = (meta.promptTokenCount as number) ?? 0;
295
- const completion = (meta.candidatesTokenCount as number) ?? 0;
296
- lastUsage = {
297
- prompt_tokens: prompt,
298
- completion_tokens: completion,
299
- total_tokens:
300
- (meta.totalTokenCount as number) ?? prompt + completion,
301
- };
294
+ const prompt = meta.promptTokenCount;
295
+ const completion = meta.candidatesTokenCount;
296
+ const total = meta.totalTokenCount;
297
+ const usage: AdapterUsage = {};
298
+
299
+ if (typeof prompt === "number") usage.prompt_tokens = prompt;
300
+ if (typeof completion === "number") usage.completion_tokens = completion;
301
+ if (typeof total === "number") {
302
+ usage.total_tokens = total;
303
+ } else if (typeof prompt === "number" && typeof completion === "number") {
304
+ usage.total_tokens = prompt + completion;
305
+ }
306
+
307
+ lastUsage = Object.keys(usage).length > 0 ? usage : undefined;
302
308
  }
303
309
 
304
310
  // Gemini signals completion when candidates have a finishReason
@@ -26,11 +26,15 @@ export interface ProviderOptions {
26
26
  requestTimeoutMs?: number;
27
27
  }
28
28
 
29
- /** Token usage at the adapter level (snake_case). */
29
+ /** Classification of where a usage figure came from. */
30
+ export type UsageSource = "reported" | "estimated" | "unavailable" | "zero";
31
+
32
+ /** Token usage at the adapter level (snake_case). Fields are optional/null-tolerant
33
+ * because providers may report partial usage or omit fields entirely. */
30
34
  export interface AdapterUsage {
31
- prompt_tokens: number;
32
- completion_tokens: number;
33
- total_tokens: number;
35
+ prompt_tokens?: number | null;
36
+ completion_tokens?: number | null;
37
+ total_tokens?: number | null;
34
38
  }
35
39
 
36
40
  /** Response returned by provider adapters. */
@@ -38,6 +42,7 @@ export interface AdapterResponse {
38
42
  content: Record<string, unknown> | string;
39
43
  text?: string;
40
44
  usage?: AdapterUsage;
45
+ usageAvailable?: boolean;
41
46
  raw?: unknown;
42
47
  }
43
48
 
@@ -51,6 +56,7 @@ export interface NormalizedUsage {
51
56
  promptTokens: number;
52
57
  completionTokens: number;
53
58
  totalTokens: number;
59
+ source: UsageSource;
54
60
  }
55
61
 
56
62
  /** Response returned by the chat() gateway. */
@@ -209,12 +215,20 @@ export interface LLMRequestCompleteEvent extends LLMRequestStartEvent {
209
215
  completionTokens: number;
210
216
  totalTokens: number;
211
217
  cost: number;
218
+ usageSource: UsageSource;
219
+ costEstimated: boolean;
212
220
  }
213
221
 
214
222
  /** Telemetry event: request error. */
215
223
  export interface LLMRequestErrorEvent extends LLMRequestStartEvent {
216
224
  duration: number;
217
225
  error: string;
226
+ promptTokens: number;
227
+ completionTokens: number;
228
+ totalTokens: number;
229
+ cost: number;
230
+ usageSource: UsageSource;
231
+ costEstimated: boolean;
218
232
  }
219
233
 
220
234
  /** Provider availability map. */
@@ -335,12 +349,20 @@ export class ProviderJsonParseError extends Error {
335
349
  provider: string;
336
350
  model: string;
337
351
  sample: string;
352
+ text?: string;
353
+ usage?: AdapterUsage;
354
+ usageAvailable?: boolean;
338
355
 
339
356
  constructor(
340
357
  provider: string,
341
358
  model: string,
342
359
  sample: string,
343
360
  message?: string,
361
+ options: {
362
+ text?: string;
363
+ usage?: AdapterUsage;
364
+ usageAvailable?: boolean;
365
+ } = {},
344
366
  ) {
345
367
  super(
346
368
  message ??
@@ -350,5 +372,8 @@ export class ProviderJsonParseError extends Error {
350
372
  this.provider = provider;
351
373
  this.model = model;
352
374
  this.sample = sample;
375
+ this.text = options.text;
376
+ this.usage = options.usage;
377
+ this.usageAvailable = options.usageAvailable;
353
378
  }
354
379
  }
@@ -10,6 +10,8 @@ import {
10
10
  tryParseJSON,
11
11
  createProviderError,
12
12
  ensureJsonResponseFormat,
13
+ isJsonResponseFormat,
14
+ extractResponseSchema,
13
15
  } from "./base.ts";
14
16
  import {
15
17
  IdleTimeoutController,
@@ -18,43 +20,14 @@ import {
18
20
  accumulateStream,
19
21
  } from "./stream-accumulator.ts";
20
22
  import { ProviderJsonParseError } from "./types.ts";
21
- import type { ProviderOptions, AdapterResponse, ResponseFormatObject } from "./types.ts";
23
+ import type { ProviderOptions, AdapterResponse } from "./types.ts";
22
24
 
23
25
  const ZAI_API_URL = "https://api.z.ai/api/paas/v4/chat/completions";
24
26
  const DEFAULT_MODEL = "glm-5";
25
27
  const DEFAULT_TEMPERATURE = 0.7;
26
28
  const DEFAULT_MAX_TOKENS = 8192;
27
- const DEFAULT_RESPONSE_FORMAT = "json";
28
29
  const DEFAULT_MAX_RETRIES = 3;
29
30
 
30
- /** Returns true when responseFormat indicates JSON output. */
31
- function isJsonMode(
32
- responseFormat: string | ResponseFormatObject | undefined,
33
- ): boolean {
34
- if (!responseFormat) return false;
35
- if (typeof responseFormat === "string") {
36
- return responseFormat === "json" || responseFormat === "json_object";
37
- }
38
- return (
39
- responseFormat.type === "json_object" ||
40
- responseFormat.json_schema != null
41
- );
42
- }
43
-
44
- /** Extracts a JSON schema object from responseFormat, if present. */
45
- function extractJsonSchema(
46
- responseFormat: string | ResponseFormatObject | undefined,
47
- ): unknown | undefined {
48
- if (
49
- responseFormat &&
50
- typeof responseFormat === "object" &&
51
- responseFormat.json_schema != null
52
- ) {
53
- return responseFormat.json_schema;
54
- }
55
- return undefined;
56
- }
57
-
58
31
  export async function zaiChat(
59
32
  options: ProviderOptions,
60
33
  ): Promise<AdapterResponse> {
@@ -63,23 +36,22 @@ export async function zaiChat(
63
36
  model = DEFAULT_MODEL,
64
37
  temperature = DEFAULT_TEMPERATURE,
65
38
  maxTokens = DEFAULT_MAX_TOKENS,
66
- responseFormat = DEFAULT_RESPONSE_FORMAT,
39
+ responseFormat,
67
40
  topP,
68
41
  stop,
69
42
  maxRetries = DEFAULT_MAX_RETRIES,
70
43
  requestTimeoutMs,
71
44
  } = options;
72
45
 
73
- const jsonMode = isJsonMode(responseFormat);
74
-
75
- // Validate JSON response format
76
- ensureJsonResponseFormat(responseFormat, "zai");
46
+ const jsonMode = isJsonResponseFormat(responseFormat);
47
+ if (jsonMode) {
48
+ ensureJsonResponseFormat(responseFormat, "zai");
49
+ }
77
50
 
78
51
  const { systemMsg, userMessages, assistantMessages } =
79
52
  extractMessages(messages);
80
53
 
81
- // Build system message with optional JSON schema injection
82
- const jsonSchema = extractJsonSchema(responseFormat);
54
+ const jsonSchema = extractResponseSchema(responseFormat);
83
55
  let systemText = systemMsg;
84
56
  if (jsonSchema) {
85
57
  const schemaStr = JSON.stringify(jsonSchema, null, 2);
@@ -180,19 +152,18 @@ export async function zaiChat(
180
152
  "zai",
181
153
  model,
182
154
  parsed.slice(0, 200),
155
+ undefined,
156
+ {
157
+ text: rawText,
158
+ usage: accumulated.usage,
159
+ },
183
160
  );
184
161
  }
185
162
 
186
- const usage = {
187
- prompt_tokens: accumulated.usage?.prompt_tokens ?? 0,
188
- completion_tokens: accumulated.usage?.completion_tokens ?? 0,
189
- total_tokens: accumulated.usage?.total_tokens ?? 0,
190
- };
191
-
192
163
  return {
193
164
  content: typeof parsed === "string" ? parsed : (parsed as Record<string, unknown>),
194
165
  text: rawText,
195
- usage,
166
+ usage: accumulated.usage,
196
167
  raw: accumulated,
197
168
  };
198
169
  } catch (err) {