@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,7 @@ import {
10
10
  tryParseJSON,
11
11
  createProviderError,
12
12
  ensureJsonResponseFormat,
13
+ isJsonResponseFormat,
13
14
  } from "./base.ts";
14
15
  import {
15
16
  IdleTimeoutController,
@@ -25,7 +26,6 @@ const ANTHROPIC_VERSION = "2023-06-01";
25
26
  const DEFAULT_MODEL = "claude-3-sonnet";
26
27
  const DEFAULT_TEMPERATURE = 0.7;
27
28
  const DEFAULT_MAX_TOKENS = 8192;
28
- const DEFAULT_RESPONSE_FORMAT = "json";
29
29
  const DEFAULT_MAX_RETRIES = 3;
30
30
 
31
31
  export async function anthropicChat(
@@ -36,15 +36,17 @@ export async function anthropicChat(
36
36
  model = DEFAULT_MODEL,
37
37
  temperature = DEFAULT_TEMPERATURE,
38
38
  maxTokens = DEFAULT_MAX_TOKENS,
39
- responseFormat = DEFAULT_RESPONSE_FORMAT,
39
+ responseFormat,
40
40
  topP,
41
41
  stop,
42
42
  maxRetries = DEFAULT_MAX_RETRIES,
43
43
  requestTimeoutMs,
44
44
  } = options;
45
45
 
46
- // Validate JSON response format — if this throws, we are not in JSON mode
47
- ensureJsonResponseFormat(responseFormat, "anthropic");
46
+ const jsonMode = isJsonResponseFormat(responseFormat);
47
+ if (jsonMode) {
48
+ ensureJsonResponseFormat(responseFormat, "anthropic");
49
+ }
48
50
 
49
51
  const { systemMsg, userMessages, assistantMessages } =
50
52
  extractMessages(messages);
@@ -86,6 +88,7 @@ export async function anthropicChat(
86
88
  model,
87
89
  apiKey ?? "",
88
90
  requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
91
+ jsonMode,
89
92
  );
90
93
  } catch (err) {
91
94
  lastError = err;
@@ -109,6 +112,7 @@ async function anthropicStreamChat(
109
112
  model: string,
110
113
  apiKey: string,
111
114
  timeoutMs: number,
115
+ jsonMode: boolean,
112
116
  ): Promise<AdapterResponse> {
113
117
  const idle = new IdleTimeoutController(timeoutMs);
114
118
 
@@ -151,16 +155,24 @@ async function anthropicStreamChat(
151
155
  const stripped = stripMarkdownFences(rawText);
152
156
  const parsed = tryParseJSON(stripped);
153
157
 
154
- if (typeof parsed === "string") {
155
- throw new ProviderJsonParseError(
156
- "anthropic",
157
- model,
158
- parsed.slice(0, 200),
159
- );
158
+ if (jsonMode) {
159
+ if (typeof parsed === "string") {
160
+ throw new ProviderJsonParseError(
161
+ "anthropic",
162
+ model,
163
+ parsed.slice(0, 200),
164
+ undefined,
165
+ {
166
+ text: rawText,
167
+ usage,
168
+ },
169
+ );
170
+ }
171
+ return { content: parsed as Record<string, unknown>, text: rawText, usage };
160
172
  }
161
173
 
162
174
  return {
163
- content: parsed as Record<string, unknown>,
175
+ content: rawText,
164
176
  text: rawText,
165
177
  usage,
166
178
  };
@@ -223,3 +223,22 @@ export function ensureJsonResponseFormat(
223
223
 
224
224
  throw new ProviderJsonModeError(providerName);
225
225
  }
226
+
227
+ export function isJsonResponseFormat(responseFormat: unknown): boolean {
228
+ if (typeof responseFormat === "string") {
229
+ return responseFormat === "json" || responseFormat === "json_object";
230
+ }
231
+ if (responseFormat && typeof responseFormat === "object") {
232
+ const obj = responseFormat as Record<string, unknown>;
233
+ return obj.type === "json_object" || obj.json_schema != null;
234
+ }
235
+ return false;
236
+ }
237
+
238
+ export function extractResponseSchema(responseFormat: unknown): unknown | undefined {
239
+ if (responseFormat && typeof responseFormat === "object") {
240
+ const obj = responseFormat as Record<string, unknown>;
241
+ if (obj.json_schema != null) return obj.json_schema;
242
+ }
243
+ return undefined;
244
+ }
@@ -6,6 +6,7 @@ import {
6
6
  ensureJsonResponseFormat,
7
7
  extractMessages,
8
8
  isRetryableError,
9
+ isJsonResponseFormat,
9
10
  sleep,
10
11
  stripMarkdownFences,
11
12
  tryParseJSON,
@@ -20,16 +21,9 @@ import type { ClaudeCodeOptions, AdapterResponse } from "./types.ts";
20
21
 
21
22
  const DEFAULT_MODEL = "sonnet";
22
23
  const DEFAULT_MAX_TURNS = 1;
23
- const DEFAULT_RESPONSE_FORMAT = "json";
24
24
  const DEFAULT_MAX_RETRIES = 3;
25
25
  const AVAILABILITY_TIMEOUT_MS = 5_000;
26
26
 
27
- const ZERO_USAGE = {
28
- prompt_tokens: 0,
29
- completion_tokens: 0,
30
- total_tokens: 0,
31
- } as const;
32
-
33
27
  export async function claudeCodeChat(
34
28
  options: ClaudeCodeOptions,
35
29
  ): Promise<AdapterResponse> {
@@ -38,11 +32,15 @@ export async function claudeCodeChat(
38
32
  model = DEFAULT_MODEL,
39
33
  maxTokens,
40
34
  maxTurns = DEFAULT_MAX_TURNS,
41
- responseFormat = DEFAULT_RESPONSE_FORMAT,
35
+ responseFormat,
42
36
  maxRetries = DEFAULT_MAX_RETRIES,
37
+ requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
43
38
  } = options;
44
39
 
45
- ensureJsonResponseFormat(responseFormat, "claudecode");
40
+ const jsonMode = isJsonResponseFormat(responseFormat);
41
+ if (jsonMode) {
42
+ ensureJsonResponseFormat(responseFormat, "claudecode");
43
+ }
46
44
 
47
45
  const { systemMsg, userMsg } = extractMessages(messages);
48
46
 
@@ -72,7 +70,7 @@ export async function claudeCodeChat(
72
70
  stderr: "pipe",
73
71
  });
74
72
 
75
- const idle = new IdleTimeoutController(DEFAULT_REQUEST_TIMEOUT_MS);
73
+ const idle = new IdleTimeoutController(requestTimeoutMs);
76
74
 
77
75
  // Kill the subprocess if the idle timeout fires
78
76
  const onAbort = () => proc.kill();
@@ -94,18 +92,29 @@ export async function claudeCodeChat(
94
92
  const cleanedText = stripMarkdownFences(accumulated.text);
95
93
  const parsed = tryParseJSON(cleanedText);
96
94
 
97
- if (typeof parsed === "string") {
98
- throw new ProviderJsonParseError(
99
- "claudecode",
100
- model,
101
- parsed.slice(0, 200),
102
- );
95
+ if (jsonMode) {
96
+ if (typeof parsed === "string") {
97
+ throw new ProviderJsonParseError(
98
+ "claudecode",
99
+ model,
100
+ parsed.slice(0, 200),
101
+ undefined,
102
+ {
103
+ text: accumulated.text,
104
+ usage: accumulated.usage,
105
+ },
106
+ );
107
+ }
108
+ return {
109
+ content: parsed as Record<string, unknown>,
110
+ text: accumulated.text,
111
+ raw: accumulated.text,
112
+ };
103
113
  }
104
114
 
105
115
  return {
106
- content: parsed as Record<string, unknown>,
116
+ content: accumulated.text,
107
117
  text: accumulated.text,
108
- usage: { ...ZERO_USAGE },
109
118
  raw: accumulated.text,
110
119
  };
111
120
  } catch (err) {
@@ -5,6 +5,7 @@ import {
5
5
  DEFAULT_REQUEST_TIMEOUT_MS,
6
6
  extractMessages,
7
7
  isRetryableError,
8
+ isJsonResponseFormat,
8
9
  sleep,
9
10
  stripMarkdownFences,
10
11
  tryParseJSON,
@@ -14,7 +15,6 @@ import { ProviderJsonParseError } from "./types.ts";
14
15
  import type {
15
16
  DeepSeekOptions,
16
17
  AdapterResponse,
17
- ResponseFormatObject,
18
18
  } from "./types.ts";
19
19
  import {
20
20
  IdleTimeoutController,
@@ -26,26 +26,8 @@ import {
26
26
  const DEEPSEEK_API_URL = "https://api.deepseek.com/chat/completions";
27
27
  const DEFAULT_MODEL = "deepseek-chat";
28
28
  const DEFAULT_TEMPERATURE = 0.7;
29
- const DEFAULT_RESPONSE_FORMAT = "json_object";
30
29
  const DEFAULT_MAX_RETRIES = 3;
31
30
 
32
- /**
33
- * Determines whether the response format indicates JSON mode.
34
- * Returns true for "json", "json_object", { type: "json_object" }, { json_schema: ... }.
35
- */
36
- function isJsonMode(
37
- responseFormat: string | ResponseFormatObject | undefined,
38
- ): boolean {
39
- if (!responseFormat) return false;
40
- if (typeof responseFormat === "string") {
41
- return responseFormat === "json" || responseFormat === "json_object";
42
- }
43
- return (
44
- responseFormat.type === "json_object" ||
45
- responseFormat.json_schema != null
46
- );
47
- }
48
-
49
31
  export async function deepseekChat(
50
32
  options: DeepSeekOptions,
51
33
  ): Promise<AdapterResponse> {
@@ -54,7 +36,7 @@ export async function deepseekChat(
54
36
  model = DEFAULT_MODEL,
55
37
  temperature = DEFAULT_TEMPERATURE,
56
38
  maxTokens,
57
- responseFormat = DEFAULT_RESPONSE_FORMAT,
39
+ responseFormat,
58
40
  topP,
59
41
  stop,
60
42
  maxRetries = DEFAULT_MAX_RETRIES,
@@ -66,7 +48,7 @@ export async function deepseekChat(
66
48
  ? Math.max(0, Math.trunc(maxRetries))
67
49
  : DEFAULT_MAX_RETRIES;
68
50
 
69
- const jsonMode = isJsonMode(responseFormat);
51
+ const jsonMode = isJsonResponseFormat(responseFormat);
70
52
 
71
53
  const { systemMsg, userMessages, assistantMessages } =
72
54
  extractMessages(messages);
@@ -159,21 +141,20 @@ export async function deepseekChat(
159
141
  "deepseek",
160
142
  model,
161
143
  parsed.slice(0, 200),
144
+ undefined,
145
+ {
146
+ text: rawText,
147
+ usage: accumulated.usage,
148
+ },
162
149
  );
163
150
  }
164
151
 
165
- const usage = accumulated.usage ?? {
166
- prompt_tokens: 0,
167
- completion_tokens: 0,
168
- total_tokens: 0,
169
- };
170
-
171
152
  return {
172
153
  content:
173
154
  typeof parsed === "string"
174
155
  ? parsed
175
156
  : (parsed as Record<string, unknown>),
176
- usage,
157
+ usage: accumulated.usage,
177
158
  };
178
159
  } catch (err) {
179
160
  lastError = err;
@@ -10,13 +10,14 @@ import {
10
10
  tryParseJSON,
11
11
  createProviderError,
12
12
  ensureJsonResponseFormat,
13
+ isJsonResponseFormat,
14
+ extractResponseSchema,
13
15
  } from "./base.ts";
14
16
  import { IdleTimeoutController, parseGeminiSse, accumulateStream } from "./stream-accumulator.ts";
15
17
  import { ProviderJsonParseError } from "./types.ts";
16
18
  import type {
17
19
  GeminiOptions,
18
20
  AdapterResponse,
19
- ResponseFormatObject,
20
21
  } from "./types.ts";
21
22
 
22
23
  const DEFAULT_MODEL = "gemini-2.5-flash";
@@ -33,39 +34,6 @@ const SAFETY_SETTINGS = [
33
34
  { category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_NONE" },
34
35
  ];
35
36
 
36
- /**
37
- * Determines whether the response format indicates JSON mode.
38
- * Returns true for "json", "json_object", { type: "json_object" }, { json_schema: ... }.
39
- */
40
- function isJsonMode(
41
- responseFormat: string | ResponseFormatObject | undefined,
42
- ): boolean {
43
- if (!responseFormat) return false;
44
- if (typeof responseFormat === "string") {
45
- return responseFormat === "json" || responseFormat === "json_object";
46
- }
47
- return (
48
- responseFormat.type === "json_object" ||
49
- responseFormat.json_schema != null
50
- );
51
- }
52
-
53
- /**
54
- * Extracts a JSON schema object from the responseFormat, if present.
55
- */
56
- function extractJsonSchema(
57
- responseFormat: string | ResponseFormatObject | undefined,
58
- ): unknown | undefined {
59
- if (
60
- responseFormat &&
61
- typeof responseFormat === "object" &&
62
- responseFormat.json_schema != null
63
- ) {
64
- return responseFormat.json_schema;
65
- }
66
- return undefined;
67
- }
68
-
69
37
  export async function geminiChat(
70
38
  options: GeminiOptions,
71
39
  ): Promise<AdapterResponse> {
@@ -84,12 +52,11 @@ export async function geminiChat(
84
52
  presencePenalty: _presencePenalty,
85
53
  } = options;
86
54
 
87
- const jsonMode = isJsonMode(responseFormat);
88
-
89
- // Validate JSON response format if one was specified
90
- if (responseFormat) {
55
+ const jsonMode = isJsonResponseFormat(responseFormat);
56
+ if (jsonMode) {
91
57
  ensureJsonResponseFormat(responseFormat, "gemini");
92
58
  }
59
+ const jsonSchema = extractResponseSchema(responseFormat);
93
60
 
94
61
  const { systemMsg, userMessages, assistantMessages } =
95
62
  extractMessages(messages);
@@ -108,25 +75,21 @@ export async function geminiChat(
108
75
  parts: [{ text: m.content }],
109
76
  }));
110
77
 
111
- // Build system instruction, optionally injecting JSON schema
112
- const jsonSchema = extractJsonSchema(responseFormat);
113
- let systemText = systemMsg;
114
- if (jsonSchema) {
115
- const schemaStr = JSON.stringify(jsonSchema, null, 2);
116
- const schemaInstruction = `\n\nRespond with JSON matching this schema:\n${schemaStr}`;
117
- systemText = systemText ? systemText + schemaInstruction : schemaInstruction.trimStart();
118
- }
119
-
120
- const systemInstruction = systemText
121
- ? { parts: [{ text: systemText }] }
78
+ const systemInstruction = systemMsg
79
+ ? { parts: [{ text: systemMsg }] }
122
80
  : undefined;
123
81
 
124
- // Build generationConfig
82
+ // Build generationConfig — wire native structured output when schema is present.
125
83
  const generationConfig: Record<string, unknown> = { temperature };
126
84
  if (maxTokens != null) generationConfig["maxOutputTokens"] = maxTokens;
127
85
  if (topP != null) generationConfig["topP"] = topP;
128
86
  if (stop != null) generationConfig["stopSequences"] = Array.isArray(stop) ? stop : [stop];
129
- if (jsonMode) generationConfig["responseMimeType"] = "application/json";
87
+ if (jsonSchema != null) {
88
+ generationConfig["responseMimeType"] = "application/json";
89
+ generationConfig["responseSchema"] = jsonSchema;
90
+ } else if (jsonMode) {
91
+ generationConfig["responseMimeType"] = "application/json";
92
+ }
130
93
 
131
94
  const baseUrl =
132
95
  process.env["GEMINI_BASE_URL"] ?? DEFAULT_BASE_URL;
@@ -189,19 +152,18 @@ export async function geminiChat(
189
152
  "gemini",
190
153
  model,
191
154
  parsed.slice(0, 200),
155
+ undefined,
156
+ {
157
+ text: rawText,
158
+ usage: streamUsage,
159
+ },
192
160
  );
193
161
  }
194
162
 
195
- const usage = streamUsage ?? {
196
- prompt_tokens: 0,
197
- completion_tokens: 0,
198
- total_tokens: 0,
199
- };
200
-
201
163
  return {
202
164
  content: typeof parsed === "string" ? parsed : (parsed as Record<string, unknown>),
203
165
  text: rawText,
204
- usage,
166
+ usage: streamUsage,
205
167
  };
206
168
  } catch (err) {
207
169
  lastError = err;
@@ -6,6 +6,7 @@ import {
6
6
  extractMessages,
7
7
  ensureMessagesPresent,
8
8
  isRetryableError,
9
+ isJsonResponseFormat,
9
10
  sleep,
10
11
  stripMarkdownFences,
11
12
  tryParseJSON,
@@ -52,6 +53,8 @@ export async function moonshotChat(
52
53
 
53
54
  ensureMessagesPresent(messages, "moonshot");
54
55
 
56
+ const jsonMode = isJsonResponseFormat(responseFormat);
57
+
55
58
  const { systemMsg, userMessages, assistantMessages } =
56
59
  extractMessages(messages);
57
60
 
@@ -116,7 +119,7 @@ export async function moonshotChat(
116
119
  return deepseekChat({
117
120
  messages,
118
121
  model: deepseekModel,
119
- responseFormat: responseFormat ?? "json_object",
122
+ responseFormat,
120
123
  requestTimeoutMs,
121
124
  });
122
125
  }
@@ -144,24 +147,25 @@ export async function moonshotChat(
144
147
  const stripped = stripMarkdownFences(rawText);
145
148
  const parsed = tryParseJSON(stripped);
146
149
 
147
- // Always in JSON mode string result means parse failure
148
- if (typeof parsed === "string") {
150
+ if (jsonMode && typeof parsed === "string") {
149
151
  throw new ProviderJsonParseError(
150
152
  "moonshot",
151
153
  model,
152
154
  parsed.slice(0, 200),
155
+ undefined,
156
+ {
157
+ text: rawText,
158
+ usage: accumulated.usage,
159
+ },
153
160
  );
154
161
  }
155
162
 
156
- const usage = accumulated.usage ?? {
157
- prompt_tokens: 0,
158
- completion_tokens: 0,
159
- total_tokens: 0,
160
- };
161
-
162
163
  return {
163
- content: parsed as Record<string, unknown>,
164
- usage,
164
+ content:
165
+ typeof parsed === "string"
166
+ ? parsed
167
+ : (parsed as Record<string, unknown>),
168
+ usage: accumulated.usage,
165
169
  raw: { accumulated: rawText },
166
170
  };
167
171
  } catch (err) {