@gajae-code/ai 0.1.1

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 (349) hide show
  1. package/CHANGELOG.md +2644 -0
  2. package/README.md +1181 -0
  3. package/dist/types/api-registry.d.ts +30 -0
  4. package/dist/types/auth-broker/client.d.ts +66 -0
  5. package/dist/types/auth-broker/index.d.ts +5 -0
  6. package/dist/types/auth-broker/refresher.d.ts +25 -0
  7. package/dist/types/auth-broker/remote-store.d.ts +96 -0
  8. package/dist/types/auth-broker/server.d.ts +32 -0
  9. package/dist/types/auth-broker/types.d.ts +105 -0
  10. package/dist/types/auth-broker/wire-schemas.d.ts +412 -0
  11. package/dist/types/auth-gateway/http.d.ts +39 -0
  12. package/dist/types/auth-gateway/index.d.ts +3 -0
  13. package/dist/types/auth-gateway/server.d.ts +17 -0
  14. package/dist/types/auth-gateway/types.d.ts +115 -0
  15. package/dist/types/auth-storage.d.ts +641 -0
  16. package/dist/types/cli.d.ts +2 -0
  17. package/dist/types/index.d.ts +49 -0
  18. package/dist/types/model-cache.d.ts +17 -0
  19. package/dist/types/model-manager.d.ts +62 -0
  20. package/dist/types/model-thinking.d.ts +71 -0
  21. package/dist/types/models.d.ts +12 -0
  22. package/dist/types/provider-details.d.ts +24 -0
  23. package/dist/types/provider-models/bundled-references.d.ts +4 -0
  24. package/dist/types/provider-models/descriptors.d.ts +48 -0
  25. package/dist/types/provider-models/google.d.ts +20 -0
  26. package/dist/types/provider-models/index.d.ts +5 -0
  27. package/dist/types/provider-models/ollama.d.ts +7 -0
  28. package/dist/types/provider-models/openai-compat.d.ts +237 -0
  29. package/dist/types/provider-models/special.d.ts +16 -0
  30. package/dist/types/providers/amazon-bedrock.d.ts +36 -0
  31. package/dist/types/providers/anthropic-messages-server-schema.d.ts +450 -0
  32. package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
  33. package/dist/types/providers/anthropic.d.ts +188 -0
  34. package/dist/types/providers/aws-credentials.d.ts +43 -0
  35. package/dist/types/providers/aws-eventstream.d.ts +38 -0
  36. package/dist/types/providers/aws-sigv4.d.ts +55 -0
  37. package/dist/types/providers/azure-openai-responses.d.ts +15 -0
  38. package/dist/types/providers/cursor/gen/agent_pb.d.ts +13022 -0
  39. package/dist/types/providers/cursor.d.ts +42 -0
  40. package/dist/types/providers/error-message.d.ts +27 -0
  41. package/dist/types/providers/github-copilot-headers.d.ts +40 -0
  42. package/dist/types/providers/gitlab-duo.d.ts +27 -0
  43. package/dist/types/providers/google-auth.d.ts +24 -0
  44. package/dist/types/providers/google-gemini-cli.d.ts +72 -0
  45. package/dist/types/providers/google-gemini-headers.d.ts +18 -0
  46. package/dist/types/providers/google-shared.d.ts +163 -0
  47. package/dist/types/providers/google-types.d.ts +138 -0
  48. package/dist/types/providers/google-vertex.d.ts +7 -0
  49. package/dist/types/providers/google.d.ts +4 -0
  50. package/dist/types/providers/grammar.d.ts +1 -0
  51. package/dist/types/providers/kimi.d.ts +27 -0
  52. package/dist/types/providers/mock.d.ts +175 -0
  53. package/dist/types/providers/ollama.d.ts +6 -0
  54. package/dist/types/providers/openai-anthropic-shim.d.ts +31 -0
  55. package/dist/types/providers/openai-chat-server-schema.d.ts +814 -0
  56. package/dist/types/providers/openai-chat-server.d.ts +16 -0
  57. package/dist/types/providers/openai-codex/constants.d.ts +26 -0
  58. package/dist/types/providers/openai-codex/request-transformer.d.ts +49 -0
  59. package/dist/types/providers/openai-codex/response-handler.d.ts +17 -0
  60. package/dist/types/providers/openai-codex-responses.d.ts +67 -0
  61. package/dist/types/providers/openai-completions-compat.d.ts +25 -0
  62. package/dist/types/providers/openai-completions.d.ts +33 -0
  63. package/dist/types/providers/openai-responses-server-schema.d.ts +392 -0
  64. package/dist/types/providers/openai-responses-server.d.ts +17 -0
  65. package/dist/types/providers/openai-responses-shared.d.ts +89 -0
  66. package/dist/types/providers/openai-responses.d.ts +32 -0
  67. package/dist/types/providers/pi-native-client.d.ts +13 -0
  68. package/dist/types/providers/pi-native-server.d.ts +68 -0
  69. package/dist/types/providers/register-builtins.d.ts +31 -0
  70. package/dist/types/providers/synthetic.d.ts +26 -0
  71. package/dist/types/providers/transform-messages.d.ts +12 -0
  72. package/dist/types/providers/vision-guard.d.ts +8 -0
  73. package/dist/types/rate-limit-utils.d.ts +19 -0
  74. package/dist/types/stream.d.ts +24 -0
  75. package/dist/types/types.d.ts +746 -0
  76. package/dist/types/usage/claude.d.ts +3 -0
  77. package/dist/types/usage/gemini.d.ts +2 -0
  78. package/dist/types/usage/github-copilot.d.ts +7 -0
  79. package/dist/types/usage/google-antigravity.d.ts +2 -0
  80. package/dist/types/usage/kimi.d.ts +2 -0
  81. package/dist/types/usage/minimax-code.d.ts +2 -0
  82. package/dist/types/usage/openai-codex.d.ts +3 -0
  83. package/dist/types/usage/shared.d.ts +1 -0
  84. package/dist/types/usage/zai.d.ts +2 -0
  85. package/dist/types/usage.d.ts +258 -0
  86. package/dist/types/utils/abort.d.ts +19 -0
  87. package/dist/types/utils/anthropic-auth.d.ts +31 -0
  88. package/dist/types/utils/discovery/antigravity.d.ts +61 -0
  89. package/dist/types/utils/discovery/codex.d.ts +38 -0
  90. package/dist/types/utils/discovery/cursor.d.ts +23 -0
  91. package/dist/types/utils/discovery/gemini.d.ts +25 -0
  92. package/dist/types/utils/discovery/index.d.ts +4 -0
  93. package/dist/types/utils/discovery/openai-compatible.d.ts +72 -0
  94. package/dist/types/utils/event-stream.d.ts +28 -0
  95. package/dist/types/utils/fireworks-model-id.d.ts +10 -0
  96. package/dist/types/utils/foundry.d.ts +1 -0
  97. package/dist/types/utils/h2-fetch.d.ts +22 -0
  98. package/dist/types/utils/http-inspector.d.ts +31 -0
  99. package/dist/types/utils/idle-iterator.d.ts +67 -0
  100. package/dist/types/utils/json-parse.d.ts +10 -0
  101. package/dist/types/utils/oauth/alibaba-coding-plan.d.ts +18 -0
  102. package/dist/types/utils/oauth/anthropic.d.ts +22 -0
  103. package/dist/types/utils/oauth/api-key-login.d.ts +35 -0
  104. package/dist/types/utils/oauth/api-key-validation.d.ts +27 -0
  105. package/dist/types/utils/oauth/callback-server.d.ts +57 -0
  106. package/dist/types/utils/oauth/cerebras.d.ts +1 -0
  107. package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +18 -0
  108. package/dist/types/utils/oauth/cursor.d.ts +15 -0
  109. package/dist/types/utils/oauth/deepseek.d.ts +10 -0
  110. package/dist/types/utils/oauth/firepass.d.ts +1 -0
  111. package/dist/types/utils/oauth/fireworks.d.ts +1 -0
  112. package/dist/types/utils/oauth/github-copilot.d.ts +38 -0
  113. package/dist/types/utils/oauth/gitlab-duo.d.ts +3 -0
  114. package/dist/types/utils/oauth/google-antigravity.d.ts +11 -0
  115. package/dist/types/utils/oauth/google-gemini-cli.d.ts +10 -0
  116. package/dist/types/utils/oauth/google-oauth-shared.d.ts +28 -0
  117. package/dist/types/utils/oauth/huggingface.d.ts +19 -0
  118. package/dist/types/utils/oauth/index.d.ts +38 -0
  119. package/dist/types/utils/oauth/kagi.d.ts +17 -0
  120. package/dist/types/utils/oauth/kilo.d.ts +5 -0
  121. package/dist/types/utils/oauth/kimi.d.ts +21 -0
  122. package/dist/types/utils/oauth/litellm.d.ts +18 -0
  123. package/dist/types/utils/oauth/lm-studio.d.ts +17 -0
  124. package/dist/types/utils/oauth/minimax-code.d.ts +28 -0
  125. package/dist/types/utils/oauth/moonshot.d.ts +1 -0
  126. package/dist/types/utils/oauth/nanogpt.d.ts +1 -0
  127. package/dist/types/utils/oauth/nvidia.d.ts +18 -0
  128. package/dist/types/utils/oauth/ollama-cloud.d.ts +2 -0
  129. package/dist/types/utils/oauth/ollama.d.ts +18 -0
  130. package/dist/types/utils/oauth/openai-codex.d.ts +21 -0
  131. package/dist/types/utils/oauth/opencode.d.ts +18 -0
  132. package/dist/types/utils/oauth/parallel.d.ts +17 -0
  133. package/dist/types/utils/oauth/perplexity.d.ts +9 -0
  134. package/dist/types/utils/oauth/pkce.d.ts +8 -0
  135. package/dist/types/utils/oauth/qianfan.d.ts +17 -0
  136. package/dist/types/utils/oauth/qwen-portal.d.ts +19 -0
  137. package/dist/types/utils/oauth/synthetic.d.ts +1 -0
  138. package/dist/types/utils/oauth/tavily.d.ts +17 -0
  139. package/dist/types/utils/oauth/together.d.ts +1 -0
  140. package/dist/types/utils/oauth/types.d.ts +44 -0
  141. package/dist/types/utils/oauth/venice.d.ts +18 -0
  142. package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +18 -0
  143. package/dist/types/utils/oauth/vllm.d.ts +16 -0
  144. package/dist/types/utils/oauth/xiaomi.d.ts +19 -0
  145. package/dist/types/utils/oauth/zai.d.ts +18 -0
  146. package/dist/types/utils/oauth/zenmux.d.ts +1 -0
  147. package/dist/types/utils/overflow.d.ts +54 -0
  148. package/dist/types/utils/parse-bind.d.ts +23 -0
  149. package/dist/types/utils/provider-response.d.ts +3 -0
  150. package/dist/types/utils/retry-after.d.ts +3 -0
  151. package/dist/types/utils/retry.d.ts +26 -0
  152. package/dist/types/utils/schema/adapt.d.ts +24 -0
  153. package/dist/types/utils/schema/compatibility.d.ts +30 -0
  154. package/dist/types/utils/schema/dereference.d.ts +11 -0
  155. package/dist/types/utils/schema/draft.d.ts +10 -0
  156. package/dist/types/utils/schema/equality.d.ts +4 -0
  157. package/dist/types/utils/schema/fields.d.ts +49 -0
  158. package/dist/types/utils/schema/index.d.ts +13 -0
  159. package/dist/types/utils/schema/json-schema-validator.d.ts +12 -0
  160. package/dist/types/utils/schema/meta-validator.d.ts +2 -0
  161. package/dist/types/utils/schema/normalize.d.ts +93 -0
  162. package/dist/types/utils/schema/spill.d.ts +8 -0
  163. package/dist/types/utils/schema/stamps.d.ts +25 -0
  164. package/dist/types/utils/schema/types.d.ts +4 -0
  165. package/dist/types/utils/schema/wire.d.ts +54 -0
  166. package/dist/types/utils/schema/zod-decontaminate.d.ts +31 -0
  167. package/dist/types/utils/sse-debug.d.ts +10 -0
  168. package/dist/types/utils/tool-call-healing.d.ts +71 -0
  169. package/dist/types/utils/tool-choice.d.ts +50 -0
  170. package/dist/types/utils/validation.d.ts +17 -0
  171. package/dist/types/utils.d.ts +28 -0
  172. package/package.json +146 -0
  173. package/src/api-registry.ts +96 -0
  174. package/src/auth-broker/client.ts +358 -0
  175. package/src/auth-broker/index.ts +5 -0
  176. package/src/auth-broker/refresher.ts +127 -0
  177. package/src/auth-broker/remote-store.ts +623 -0
  178. package/src/auth-broker/server.ts +644 -0
  179. package/src/auth-broker/types.ts +127 -0
  180. package/src/auth-broker/wire-schemas.ts +200 -0
  181. package/src/auth-gateway/http.ts +194 -0
  182. package/src/auth-gateway/index.ts +3 -0
  183. package/src/auth-gateway/server.ts +717 -0
  184. package/src/auth-gateway/types.ts +134 -0
  185. package/src/auth-storage.ts +4104 -0
  186. package/src/cli.ts +262 -0
  187. package/src/index.ts +54 -0
  188. package/src/model-cache.ts +129 -0
  189. package/src/model-manager.ts +450 -0
  190. package/src/model-thinking.ts +691 -0
  191. package/src/models.json +73853 -0
  192. package/src/models.json.d.ts +9 -0
  193. package/src/models.ts +56 -0
  194. package/src/prompts/turn-aborted-guidance.md +4 -0
  195. package/src/provider-details.ts +90 -0
  196. package/src/provider-models/bundled-references.ts +38 -0
  197. package/src/provider-models/descriptors.ts +308 -0
  198. package/src/provider-models/google.ts +91 -0
  199. package/src/provider-models/index.ts +5 -0
  200. package/src/provider-models/ollama.ts +153 -0
  201. package/src/provider-models/openai-compat.ts +2275 -0
  202. package/src/provider-models/special.ts +67 -0
  203. package/src/providers/amazon-bedrock.ts +849 -0
  204. package/src/providers/anthropic-messages-server-schema.ts +229 -0
  205. package/src/providers/anthropic-messages-server.ts +677 -0
  206. package/src/providers/anthropic.ts +2696 -0
  207. package/src/providers/aws-credentials.ts +501 -0
  208. package/src/providers/aws-eventstream.ts +185 -0
  209. package/src/providers/aws-sigv4.ts +218 -0
  210. package/src/providers/azure-openai-responses.ts +337 -0
  211. package/src/providers/cursor/gen/agent_pb.ts +15274 -0
  212. package/src/providers/cursor/proto/agent.proto +3526 -0
  213. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  214. package/src/providers/cursor/proto/buf.yaml +17 -0
  215. package/src/providers/cursor.ts +2561 -0
  216. package/src/providers/error-message.ts +21 -0
  217. package/src/providers/github-copilot-headers.ts +140 -0
  218. package/src/providers/gitlab-duo.ts +372 -0
  219. package/src/providers/google-auth.ts +252 -0
  220. package/src/providers/google-gemini-cli.ts +795 -0
  221. package/src/providers/google-gemini-headers.ts +41 -0
  222. package/src/providers/google-shared.ts +902 -0
  223. package/src/providers/google-types.ts +167 -0
  224. package/src/providers/google-vertex.ts +88 -0
  225. package/src/providers/google.ts +41 -0
  226. package/src/providers/grammar.ts +70 -0
  227. package/src/providers/kimi.ts +52 -0
  228. package/src/providers/mock.ts +500 -0
  229. package/src/providers/ollama.ts +544 -0
  230. package/src/providers/openai-anthropic-shim.ts +138 -0
  231. package/src/providers/openai-chat-server-schema.ts +243 -0
  232. package/src/providers/openai-chat-server.ts +628 -0
  233. package/src/providers/openai-codex/constants.ts +43 -0
  234. package/src/providers/openai-codex/request-transformer.ts +161 -0
  235. package/src/providers/openai-codex/response-handler.ts +81 -0
  236. package/src/providers/openai-codex-responses.ts +2598 -0
  237. package/src/providers/openai-completions-compat.ts +279 -0
  238. package/src/providers/openai-completions.ts +1853 -0
  239. package/src/providers/openai-responses-server-schema.ts +290 -0
  240. package/src/providers/openai-responses-server.ts +1183 -0
  241. package/src/providers/openai-responses-shared.ts +800 -0
  242. package/src/providers/openai-responses.ts +621 -0
  243. package/src/providers/pi-native-client.ts +228 -0
  244. package/src/providers/pi-native-server.ts +210 -0
  245. package/src/providers/register-builtins.ts +412 -0
  246. package/src/providers/synthetic.ts +50 -0
  247. package/src/providers/transform-messages.ts +309 -0
  248. package/src/providers/vision-guard.ts +31 -0
  249. package/src/rate-limit-utils.ts +84 -0
  250. package/src/stream.ts +895 -0
  251. package/src/types.ts +884 -0
  252. package/src/usage/claude.ts +431 -0
  253. package/src/usage/gemini.ts +250 -0
  254. package/src/usage/github-copilot.ts +421 -0
  255. package/src/usage/google-antigravity.ts +201 -0
  256. package/src/usage/kimi.ts +271 -0
  257. package/src/usage/minimax-code.ts +31 -0
  258. package/src/usage/openai-codex.ts +503 -0
  259. package/src/usage/shared.ts +10 -0
  260. package/src/usage/zai.ts +247 -0
  261. package/src/usage.ts +183 -0
  262. package/src/utils/abort.ts +51 -0
  263. package/src/utils/anthropic-auth.ts +87 -0
  264. package/src/utils/discovery/antigravity.ts +261 -0
  265. package/src/utils/discovery/codex.ts +371 -0
  266. package/src/utils/discovery/cursor.ts +306 -0
  267. package/src/utils/discovery/gemini.ts +248 -0
  268. package/src/utils/discovery/index.ts +4 -0
  269. package/src/utils/discovery/openai-compatible.ts +224 -0
  270. package/src/utils/event-stream.ts +142 -0
  271. package/src/utils/fireworks-model-id.ts +30 -0
  272. package/src/utils/foundry.ts +8 -0
  273. package/src/utils/h2-fetch.ts +60 -0
  274. package/src/utils/http-inspector.ts +176 -0
  275. package/src/utils/idle-iterator.ts +250 -0
  276. package/src/utils/json-parse.ts +148 -0
  277. package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
  278. package/src/utils/oauth/anthropic.ts +200 -0
  279. package/src/utils/oauth/api-key-login.ts +87 -0
  280. package/src/utils/oauth/api-key-validation.ts +92 -0
  281. package/src/utils/oauth/callback-server.ts +276 -0
  282. package/src/utils/oauth/cerebras.ts +16 -0
  283. package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
  284. package/src/utils/oauth/cursor.ts +157 -0
  285. package/src/utils/oauth/deepseek.ts +53 -0
  286. package/src/utils/oauth/firepass.ts +24 -0
  287. package/src/utils/oauth/fireworks.ts +15 -0
  288. package/src/utils/oauth/github-copilot.ts +362 -0
  289. package/src/utils/oauth/gitlab-duo.ts +123 -0
  290. package/src/utils/oauth/google-antigravity.ts +200 -0
  291. package/src/utils/oauth/google-gemini-cli.ts +256 -0
  292. package/src/utils/oauth/google-oauth-shared.ts +110 -0
  293. package/src/utils/oauth/huggingface.ts +62 -0
  294. package/src/utils/oauth/index.ts +444 -0
  295. package/src/utils/oauth/kagi.ts +47 -0
  296. package/src/utils/oauth/kilo.ts +87 -0
  297. package/src/utils/oauth/kimi.ts +254 -0
  298. package/src/utils/oauth/litellm.ts +47 -0
  299. package/src/utils/oauth/lm-studio.ts +38 -0
  300. package/src/utils/oauth/minimax-code.ts +78 -0
  301. package/src/utils/oauth/moonshot.ts +16 -0
  302. package/src/utils/oauth/nanogpt.ts +15 -0
  303. package/src/utils/oauth/nvidia.ts +70 -0
  304. package/src/utils/oauth/oauth.html +199 -0
  305. package/src/utils/oauth/ollama-cloud.ts +28 -0
  306. package/src/utils/oauth/ollama.ts +47 -0
  307. package/src/utils/oauth/openai-codex.ts +299 -0
  308. package/src/utils/oauth/opencode.ts +49 -0
  309. package/src/utils/oauth/parallel.ts +46 -0
  310. package/src/utils/oauth/perplexity.ts +206 -0
  311. package/src/utils/oauth/pkce.ts +18 -0
  312. package/src/utils/oauth/qianfan.ts +58 -0
  313. package/src/utils/oauth/qwen-portal.ts +60 -0
  314. package/src/utils/oauth/synthetic.ts +16 -0
  315. package/src/utils/oauth/tavily.ts +46 -0
  316. package/src/utils/oauth/together.ts +16 -0
  317. package/src/utils/oauth/types.ts +94 -0
  318. package/src/utils/oauth/venice.ts +59 -0
  319. package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
  320. package/src/utils/oauth/vllm.ts +40 -0
  321. package/src/utils/oauth/xiaomi.ts +137 -0
  322. package/src/utils/oauth/zai.ts +60 -0
  323. package/src/utils/oauth/zenmux.ts +15 -0
  324. package/src/utils/overflow.ts +137 -0
  325. package/src/utils/parse-bind.ts +54 -0
  326. package/src/utils/provider-response.ts +30 -0
  327. package/src/utils/retry-after.ts +110 -0
  328. package/src/utils/retry.ts +54 -0
  329. package/src/utils/schema/CONSTRAINTS.md +164 -0
  330. package/src/utils/schema/adapt.ts +36 -0
  331. package/src/utils/schema/compatibility.ts +435 -0
  332. package/src/utils/schema/dereference.ts +98 -0
  333. package/src/utils/schema/draft.ts +341 -0
  334. package/src/utils/schema/equality.ts +97 -0
  335. package/src/utils/schema/fields.ts +190 -0
  336. package/src/utils/schema/index.ts +13 -0
  337. package/src/utils/schema/json-schema-validator.ts +577 -0
  338. package/src/utils/schema/meta-validator.ts +167 -0
  339. package/src/utils/schema/normalize.ts +1588 -0
  340. package/src/utils/schema/spill.ts +43 -0
  341. package/src/utils/schema/stamps.ts +97 -0
  342. package/src/utils/schema/types.ts +11 -0
  343. package/src/utils/schema/wire.ts +213 -0
  344. package/src/utils/schema/zod-decontaminate.ts +331 -0
  345. package/src/utils/sse-debug.ts +289 -0
  346. package/src/utils/tool-call-healing.ts +271 -0
  347. package/src/utils/tool-choice.ts +99 -0
  348. package/src/utils/validation.ts +1019 -0
  349. package/src/utils.ts +166 -0
@@ -0,0 +1,1183 @@
1
+ /**
2
+ * OpenAI Responses HTTP wire-format ↔ gjc Context bridge for the auth-gateway.
3
+ *
4
+ * Inbound: parses `POST /v1/responses` request bodies into a {@link ParsedRequest}.
5
+ * Outbound: encodes gjc's {@link AssistantMessage} (and event stream) back into
6
+ * the documented `response.*` SSE taxonomy or the non-streaming JSON shape.
7
+ *
8
+ * Spec: https://platform.openai.com/docs/api-reference/responses
9
+ * Inverse direction (source-of-truth for item shapes): ../../providers/openai-responses.ts
10
+ */
11
+
12
+ import { logger } from "@gajae-code/utils";
13
+ import { resolvePromptCacheKey } from "../auth-gateway/http";
14
+ import type { AuthGatewayParsedRequest as ParsedRequest } from "../auth-gateway/types";
15
+ import type {
16
+ AssistantMessage,
17
+ AssistantMessageEventStream,
18
+ Context,
19
+ Message,
20
+ TextContent,
21
+ ThinkingContent,
22
+ Tool,
23
+ ToolCall,
24
+ } from "../types";
25
+ import {
26
+ type OpenAIResponsesFunctionCallItem,
27
+ type OpenAIResponsesFunctionCallOutputItem,
28
+ type OpenAIResponsesInputContent,
29
+ type OpenAIResponsesOutputContent,
30
+ type OpenAIResponsesReasoningItem,
31
+ type OpenAIResponsesTool,
32
+ openaiResponsesRequestSchema,
33
+ } from "./openai-responses-server-schema";
34
+
35
+ export type { ParsedRequest };
36
+
37
+ // ─── narrow guards ──────────────────────────────────────────────────────────
38
+
39
+ function isReasoningEffort(value: unknown): value is NonNullable<ParsedRequest["options"]["reasoning"]> {
40
+ return value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh";
41
+ }
42
+
43
+ function isServiceTier(value: unknown): value is NonNullable<ParsedRequest["options"]["serviceTier"]> {
44
+ return value === "auto" || value === "default" || value === "flex" || value === "scale" || value === "priority";
45
+ }
46
+
47
+ function isObj(v: unknown): v is Record<string, unknown> {
48
+ return typeof v === "object" && v !== null && !Array.isArray(v);
49
+ }
50
+
51
+ function asString(v: unknown): string | undefined {
52
+ return typeof v === "string" ? v : undefined;
53
+ }
54
+
55
+ // ─── id helpers ─────────────────────────────────────────────────────────────
56
+
57
+ function uuidNoDashes(): string {
58
+ return crypto.randomUUID().replace(/-/g, "");
59
+ }
60
+
61
+ function makeRespId(): string {
62
+ return `resp_${uuidNoDashes()}`;
63
+ }
64
+
65
+ function makeMsgId(): string {
66
+ return `msg_${uuidNoDashes()}`;
67
+ }
68
+
69
+ function makeReasoningId(): string {
70
+ return `rs_${uuidNoDashes()}`;
71
+ }
72
+
73
+ function makeFuncCallId(): string {
74
+ return `fc_${uuidNoDashes()}`;
75
+ }
76
+
77
+ function makeCustomCallId(): string {
78
+ return `ctc_${uuidNoDashes()}`;
79
+ }
80
+
81
+ // ─── once-only warnings ─────────────────────────────────────────────────────
82
+ // Module-scoped so we don't spam logs once per turn.
83
+
84
+ let warnedImageNotSupported = false;
85
+ let warnedFileNotSupported = false;
86
+ let warnedReasoningSummaryLevel = false;
87
+
88
+ // ─── inbound parser helpers ─────────────────────────────────────────────────
89
+
90
+ function extractReasoningTextFromItem(item: OpenAIResponsesReasoningItem): string {
91
+ // Prefer `summary[]` — mirrors real OpenAI and the openai-responses provider
92
+ // which writes the surfaced reasoning summary into `summary[].text`.
93
+ const fromSummary = (item.summary ?? []).map(c => c.text).join("");
94
+ if (fromSummary) return fromSummary;
95
+ return (item.content ?? []).map(c => c.text).join("");
96
+ }
97
+
98
+ type InputBlockUnion =
99
+ | { type: "input_text"; text: string }
100
+ | { type: "text"; text: string }
101
+ | { type: "input_image"; detail?: "auto" | "low" | "high"; image_url?: string; file_id?: string }
102
+ | { type: "input_file"; file_id?: string; filename?: string; file_data?: string };
103
+
104
+ /**
105
+ * Walk an input message's content array and produce pi-ai's `TextContent[]`.
106
+ * `input_image`/`input_file` blocks become bracketed text placeholders since
107
+ * pi-ai's `ImageContent` only carries inline base64 data and we have no
108
+ * resolver for OpenAI `image_url` / `file_id` references. Logs once per kind.
109
+ */
110
+ function inputContentParts(blocks: OpenAIResponsesInputContent[] | string | undefined): string | TextContent[] {
111
+ if (typeof blocks === "string") return blocks;
112
+ if (!blocks) return [];
113
+ const parts: TextContent[] = [];
114
+ for (const raw of blocks) {
115
+ const block = raw as InputBlockUnion;
116
+ if (block.type === "input_text" || block.type === "text") {
117
+ parts.push({ type: "text", text: block.text });
118
+ } else if (block.type === "input_image") {
119
+ if (!warnedImageNotSupported) {
120
+ warnedImageNotSupported = true;
121
+ logger.warn("openai-responses-server: input_image dropped (no pi-ai bridge for image_url/file_id)", {
122
+ hasUrl: typeof block.image_url === "string",
123
+ hasFileId: typeof block.file_id === "string",
124
+ });
125
+ }
126
+ const ref = block.image_url ?? block.file_id ?? "?";
127
+ parts.push({ type: "text", text: `[image: ${ref}]` });
128
+ } else if (block.type === "input_file") {
129
+ if (!warnedFileNotSupported) {
130
+ warnedFileNotSupported = true;
131
+ logger.warn("openai-responses-server: input_file dropped (no pi-ai bridge for file_id/file_data)", {
132
+ hasFileId: typeof block.file_id === "string",
133
+ hasFileData: typeof block.file_data === "string",
134
+ });
135
+ }
136
+ const ref = block.file_id ?? block.filename ?? "?";
137
+ parts.push({ type: "text", text: `[file: ${ref}]` });
138
+ }
139
+ }
140
+ return parts.length === 1 ? parts[0].text : parts;
141
+ }
142
+
143
+ type OutputBlockUnion =
144
+ | { type: "output_text"; text: string }
145
+ | { type: "text"; text: string }
146
+ | { type: "refusal"; refusal: string };
147
+
148
+ function outputTextOf(blocks: OpenAIResponsesOutputContent[] | string | undefined): TextContent[] {
149
+ if (typeof blocks === "string") return blocks.length > 0 ? [{ type: "text", text: blocks }] : [];
150
+ if (!blocks) return [];
151
+ const out: TextContent[] = [];
152
+ for (const raw of blocks) {
153
+ const block = raw as OutputBlockUnion;
154
+ if (block.type === "output_text" || block.type === "text") {
155
+ out.push({ type: "text", text: block.text });
156
+ } else if (block.type === "refusal") {
157
+ // Preserve the refusal reason so history replay still carries it.
158
+ out.push({ type: "text", text: `[refusal: ${block.refusal}]` });
159
+ }
160
+ }
161
+ return out;
162
+ }
163
+
164
+ // The schema accepts a much wider tool_choice union than the SDK type so the
165
+ // walker narrows against the local schema shape.
166
+ type ParsedToolChoice =
167
+ | "auto"
168
+ | "none"
169
+ | "required"
170
+ | { type: "function"; name: string }
171
+ | { type: "custom"; name: string }
172
+ | {
173
+ type:
174
+ | "web_search_preview"
175
+ | "file_search"
176
+ | "computer_use_preview"
177
+ | "code_interpreter"
178
+ | "image_generation"
179
+ | "mcp";
180
+ }
181
+ | { type: "allowed_tools"; mode: "auto" | "required"; tools: Array<{ type: string; name?: string }> };
182
+
183
+ function mapToolChoice(value: ParsedToolChoice | undefined): ParsedRequest["options"]["toolChoice"] {
184
+ if (value === undefined) return undefined;
185
+ if (value === "auto" || value === "none" || value === "required") return value;
186
+ if ("type" in value) {
187
+ // `custom` (OpenAI code backend apply_patch) and `function` both resolve to the same
188
+ // pi-ai shape: pi-ai's dispatcher matches `Tool.name` AND `customWireName`,
189
+ // so passing the wire name works for either.
190
+ if (value.type === "function" || value.type === "custom") return { name: value.name };
191
+ // Hosted tools + allowed_tools — we don't surface these to pi-ai; fall
192
+ // back to letting the model pick a tool freely.
193
+ return "auto";
194
+ }
195
+ return undefined;
196
+ }
197
+
198
+ function buildTools(tools: Array<OpenAIResponsesTool | { type: string }> | undefined): Tool[] | undefined {
199
+ if (!tools) return undefined;
200
+ const out: Tool[] = [];
201
+ for (const t of tools) {
202
+ // Skip non-function tools (web_search, file_search, …).
203
+ if (t.type !== "function") continue;
204
+ const fn = t as Extract<OpenAIResponsesTool, { type: "function" }>;
205
+ const tool: Tool = {
206
+ name: fn.name,
207
+ description: fn.description ?? "",
208
+ parameters: (fn.parameters ?? {}) as Tool["parameters"],
209
+ };
210
+ if (fn.strict !== undefined && fn.strict !== null) tool.strict = fn.strict;
211
+ out.push(tool);
212
+ }
213
+ return out.length > 0 ? out : undefined;
214
+ }
215
+
216
+ function ensureAssistantPlaceholder(messages: Message[], modelId: string, now: number): AssistantMessage {
217
+ const last = messages[messages.length - 1];
218
+ if (last && last.role === "assistant") return last;
219
+ const placeholder: AssistantMessage = {
220
+ role: "assistant",
221
+ content: [],
222
+ api: "openai-responses",
223
+ provider: "openai",
224
+ model: modelId,
225
+ usage: {
226
+ input: 0,
227
+ output: 0,
228
+ cacheRead: 0,
229
+ cacheWrite: 0,
230
+ totalTokens: 0,
231
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
232
+ },
233
+ stopReason: "stop",
234
+ timestamp: now,
235
+ };
236
+ messages.push(placeholder);
237
+ return placeholder;
238
+ }
239
+
240
+ /** Flatten a function_call_output array form (text + refusal) into a single string. */
241
+ function flattenFunctionOutputArray(blocks: readonly unknown[]): string {
242
+ const parts: string[] = [];
243
+ for (const raw of blocks) {
244
+ if (!isObj(raw)) continue;
245
+ const t = raw.type;
246
+ if (t === "output_text" || t === "text") {
247
+ const text = asString(raw.text);
248
+ if (text) parts.push(text);
249
+ } else if (t === "refusal") {
250
+ const refusal = asString(raw.refusal);
251
+ if (refusal) parts.push(`[refusal: ${refusal}]`);
252
+ }
253
+ }
254
+ return parts.join("");
255
+ }
256
+
257
+ // ─── parseRequest ───────────────────────────────────────────────────────────
258
+
259
+ export function parseRequest(body: unknown, headers?: Headers): ParsedRequest {
260
+ // Header capture is centralized in `auth-gateway/server.ts` (the
261
+ // allow-listed set lands on `options.headers` automatically). We also
262
+ // consult `headers` here to populate `options.promptCacheKey` when the
263
+ // client signals a cache identity outside the body — see the
264
+ // `resolvePromptCacheKey` call further down.
265
+
266
+ const parsed = openaiResponsesRequestSchema.safeParse(body);
267
+ if (!parsed.success) {
268
+ throw new Error(`openai-responses: ${parsed.error.message}`);
269
+ }
270
+ const data = parsed.data;
271
+
272
+ const now = Date.now();
273
+ const messages: Message[] = [];
274
+ const systemPrompt: string[] = [];
275
+
276
+ if (typeof data.instructions === "string" && data.instructions.length > 0) {
277
+ systemPrompt.push(data.instructions);
278
+ }
279
+
280
+ if (typeof data.input === "string") {
281
+ messages.push({ role: "user", content: data.input, timestamp: now });
282
+ } else if (data.input) {
283
+ for (const item of data.input) {
284
+ // Items may omit `type` and rely on `role` (the convenience shape).
285
+ const effectiveType = item.type ?? ("role" in item ? "message" : undefined);
286
+ if (effectiveType === "message") {
287
+ const msg = item as {
288
+ role?: string;
289
+ content?: OpenAIResponsesInputContent[] | OpenAIResponsesOutputContent[] | string;
290
+ };
291
+ switch (msg.role) {
292
+ case "system": {
293
+ const text = inputContentParts(msg.content as OpenAIResponsesInputContent[] | string | undefined);
294
+ const flat = typeof text === "string" ? text : text.map(p => p.text).join("");
295
+ if (flat.length > 0) systemPrompt.push(flat);
296
+ break;
297
+ }
298
+ case "user":
299
+ case "developer": {
300
+ const content = inputContentParts(msg.content as OpenAIResponsesInputContent[] | string | undefined);
301
+ messages.push({ role: msg.role, content, timestamp: now });
302
+ break;
303
+ }
304
+ case "assistant": {
305
+ const parts = outputTextOf(msg.content as OpenAIResponsesOutputContent[] | string | undefined);
306
+ messages.push({
307
+ role: "assistant",
308
+ content: parts,
309
+ api: "openai-responses",
310
+ provider: "openai",
311
+ model: data.model,
312
+ usage: {
313
+ input: 0,
314
+ output: 0,
315
+ cacheRead: 0,
316
+ cacheWrite: 0,
317
+ totalTokens: 0,
318
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
319
+ },
320
+ stopReason: "stop",
321
+ timestamp: now,
322
+ });
323
+ break;
324
+ }
325
+ }
326
+ continue;
327
+ }
328
+ if (effectiveType === "reasoning") {
329
+ const reasoning = item as OpenAIResponsesReasoningItem;
330
+ const text = extractReasoningTextFromItem(reasoning);
331
+ const thinking: ThinkingContent = {
332
+ type: "thinking",
333
+ thinking: text,
334
+ thinkingSignature: JSON.stringify(reasoning),
335
+ ...(reasoning.id ? { itemId: reasoning.id } : {}),
336
+ };
337
+ ensureAssistantPlaceholder(messages, data.model, now).content.push(thinking);
338
+ continue;
339
+ }
340
+ if (effectiveType === "function_call") {
341
+ const call = item as OpenAIResponsesFunctionCallItem;
342
+ const argsRaw = call.arguments ?? "{}";
343
+ let args: Record<string, unknown>;
344
+ try {
345
+ const parsedArgs: unknown = JSON.parse(argsRaw);
346
+ args = isObj(parsedArgs) ? parsedArgs : {};
347
+ } catch {
348
+ throw new Error(`openai-responses: function_call ${call.call_id} has invalid JSON arguments`);
349
+ }
350
+ const toolCall: ToolCall = {
351
+ type: "toolCall",
352
+ id: call.call_id,
353
+ name: call.name,
354
+ arguments: args,
355
+ ...(call.id ? { thoughtSignature: call.id } : {}),
356
+ };
357
+ ensureAssistantPlaceholder(messages, data.model, now).content.push(toolCall);
358
+ continue;
359
+ }
360
+ if (effectiveType === "custom_tool_call") {
361
+ const call = item as { id?: string; call_id: string; name: string; input: string };
362
+ // Custom tools carry a raw input string. We stash it in `arguments.input`
363
+ // matching pi-ai's openai-responses-shared convention, and tag the call
364
+ // with `customWireName` so encoders re-emit it as `custom_tool_call`.
365
+ const toolCall: ToolCall = {
366
+ type: "toolCall",
367
+ id: call.call_id,
368
+ name: call.name,
369
+ arguments: { input: call.input ?? "" },
370
+ customWireName: call.name,
371
+ ...(call.id ? { thoughtSignature: call.id } : {}),
372
+ };
373
+ ensureAssistantPlaceholder(messages, data.model, now).content.push(toolCall);
374
+ continue;
375
+ }
376
+ if (effectiveType === "function_call_output") {
377
+ const output = item as OpenAIResponsesFunctionCallOutputItem;
378
+ const toolName = findToolNameById(messages, output.call_id);
379
+ const text =
380
+ typeof output.output === "string"
381
+ ? output.output
382
+ : Array.isArray(output.output)
383
+ ? flattenFunctionOutputArray(output.output)
384
+ : "";
385
+ messages.push({
386
+ role: "toolResult",
387
+ toolCallId: output.call_id,
388
+ toolName,
389
+ content: [{ type: "text", text }],
390
+ isError: false,
391
+ timestamp: now,
392
+ });
393
+ continue;
394
+ }
395
+ if (effectiveType === "custom_tool_call_output") {
396
+ const output = item as { call_id: string; output: string };
397
+ const toolName = findToolNameById(messages, output.call_id);
398
+ messages.push({
399
+ role: "toolResult",
400
+ toolCallId: output.call_id,
401
+ toolName,
402
+ content: [{ type: "text", text: output.output ?? "" }],
403
+ isError: false,
404
+ timestamp: now,
405
+ });
406
+ }
407
+ // Other item types are tolerated but not bridged.
408
+ }
409
+ }
410
+
411
+ const tools = buildTools(data.tools);
412
+ const context: Context = {
413
+ ...(systemPrompt.length > 0 ? { systemPrompt } : {}),
414
+ messages,
415
+ ...(tools ? { tools } : {}),
416
+ };
417
+
418
+ const options: ParsedRequest["options"] = {};
419
+ if (data.max_output_tokens !== undefined) options.maxOutputTokens = data.max_output_tokens;
420
+ if (data.temperature !== undefined) options.temperature = data.temperature;
421
+ if (data.top_p !== undefined) options.topP = data.top_p;
422
+ if (data.stop !== undefined && data.stop !== null) {
423
+ options.stopSequences = typeof data.stop === "string" ? [data.stop] : data.stop;
424
+ }
425
+ const toolChoice = mapToolChoice(data.tool_choice as ParsedToolChoice | undefined);
426
+ if (toolChoice !== undefined) options.toolChoice = toolChoice;
427
+ if (data.reasoning?.effort && isReasoningEffort(data.reasoning.effort)) {
428
+ options.reasoning = data.reasoning.effort;
429
+ }
430
+ // OpenAI summary: `none` → suppress; `auto`/`concise`/`detailed` → request
431
+ // visible summary. pi-ai has no per-level plumbing — log once and let the
432
+ // provider default kick in.
433
+ if (data.reasoning?.summary === "none") {
434
+ options.hideThinkingSummary = true;
435
+ } else if (
436
+ data.reasoning?.summary === "auto" ||
437
+ data.reasoning?.summary === "concise" ||
438
+ data.reasoning?.summary === "detailed"
439
+ ) {
440
+ if (!warnedReasoningSummaryLevel) {
441
+ warnedReasoningSummaryLevel = true;
442
+ logger.debug("openai-responses-server: reasoning.summary level not differentiated", {
443
+ level: data.reasoning.summary,
444
+ });
445
+ }
446
+ }
447
+ if (data.service_tier !== undefined && isServiceTier(data.service_tier)) {
448
+ options.serviceTier = data.service_tier;
449
+ }
450
+ if (data.presence_penalty !== undefined) options.presencePenalty = data.presence_penalty;
451
+ if (data.frequency_penalty !== undefined) options.frequencyPenalty = data.frequency_penalty;
452
+ if (data.parallel_tool_calls !== undefined) options.parallelToolCalls = data.parallel_tool_calls;
453
+ const cacheKey = resolvePromptCacheKey(body, headers);
454
+ if (cacheKey !== undefined) options.promptCacheKey = cacheKey;
455
+ if (data.previous_response_id !== undefined) options.previousResponseId = data.previous_response_id;
456
+ if (data.user !== undefined) options.user = data.user;
457
+ if (isObj(data.metadata)) options.metadata = data.metadata;
458
+ // `store` is a stateful-storage hint that gjc's gateway doesn't honour;
459
+ // silently accepted by the schema. No typed slot — drop.
460
+
461
+ return {
462
+ modelId: data.model,
463
+ context,
464
+ stream: data.stream === true,
465
+ options,
466
+ };
467
+ }
468
+
469
+ function findToolNameById(messages: Message[], callId: string): string {
470
+ for (let i = messages.length - 1; i >= 0; i--) {
471
+ const m = messages[i];
472
+ if (m.role !== "assistant") continue;
473
+ for (const c of m.content) {
474
+ if (c.type === "toolCall" && c.id === callId) return c.name;
475
+ }
476
+ }
477
+ return "";
478
+ }
479
+
480
+ // ─── formatError ────────────────────────────────────────────────────────────
481
+
482
+ export function formatError(status: number, type: string, message: string): Response {
483
+ return new Response(JSON.stringify({ error: { message, type } }), {
484
+ status,
485
+ headers: { "Content-Type": "application/json" },
486
+ });
487
+ }
488
+
489
+ // ─── output item builders (shared by streaming + non-streaming encoders) ────
490
+
491
+ type ReasoningOutputItem = {
492
+ type: "reasoning";
493
+ id: string;
494
+ summary: Array<{ type: "summary_text"; text: string }>;
495
+ } & Record<string, unknown>;
496
+
497
+ type MessageOutputItem = {
498
+ type: "message";
499
+ id: string;
500
+ role: "assistant";
501
+ status: "completed";
502
+ content: Array<{ type: "output_text"; text: string; annotations: never[] }>;
503
+ };
504
+
505
+ type FunctionCallOutputItem = {
506
+ type: "function_call";
507
+ id: string;
508
+ call_id: string;
509
+ name: string;
510
+ arguments: string;
511
+ status: "completed";
512
+ };
513
+
514
+ type CustomToolCallOutputItem = {
515
+ type: "custom_tool_call";
516
+ id: string;
517
+ call_id: string;
518
+ name: string;
519
+ input: string;
520
+ status: "completed";
521
+ };
522
+
523
+ type OutputItem = ReasoningOutputItem | MessageOutputItem | FunctionCallOutputItem | CustomToolCallOutputItem;
524
+
525
+ type ResponseStatus = "completed" | "in_progress" | "failed" | "incomplete";
526
+
527
+ function responseStatusForStopReason(message: AssistantMessage): ResponseStatus {
528
+ if (message.stopReason === "length") return "incomplete";
529
+ if (message.stopReason === "error" || message.stopReason === "aborted") return "failed";
530
+ return "completed";
531
+ }
532
+
533
+ function buildReasoningItem(part: ThinkingContent): ReasoningOutputItem {
534
+ const baseId = part.itemId ?? makeReasoningId();
535
+ if (part.thinkingSignature) {
536
+ try {
537
+ const sigParsed: unknown = JSON.parse(part.thinkingSignature);
538
+ if (isObj(sigParsed) && sigParsed.type === "reasoning") {
539
+ const id = part.itemId ?? asString(sigParsed.id) ?? makeReasoningId();
540
+ // Preserve any extra fields (encrypted_content, …) the original carried,
541
+ // but normalize the summary into the canonical `{type, text}[]` shape.
542
+ const merged: Record<string, unknown> = { ...sigParsed, type: "reasoning", id };
543
+ merged.summary = [{ type: "summary_text", text: part.thinking }];
544
+ // `content[]` is the encrypted/raw side-channel; leave whatever was
545
+ // already there. If absent, omit — real OpenAI only emits `content[]`
546
+ // when `include=['reasoning.encrypted_content']` is set.
547
+ return merged as ReasoningOutputItem;
548
+ }
549
+ } catch {
550
+ // Not a serialized Responses reasoning item; fall through to fresh build.
551
+ }
552
+ }
553
+ return {
554
+ type: "reasoning",
555
+ id: baseId,
556
+ summary: [{ type: "summary_text", text: part.thinking }],
557
+ };
558
+ }
559
+
560
+ function reasoningItemId(part: ThinkingContent): string {
561
+ if (part.itemId) return part.itemId;
562
+ if (part.thinkingSignature) {
563
+ try {
564
+ const sigParsed: unknown = JSON.parse(part.thinkingSignature);
565
+ if (isObj(sigParsed)) {
566
+ const id = asString(sigParsed.id);
567
+ if (id) return id;
568
+ }
569
+ } catch {
570
+ // Not a serialized Responses reasoning item.
571
+ }
572
+ }
573
+ return makeReasoningId();
574
+ }
575
+
576
+ /**
577
+ * Walk the assistant content array and group consecutive TextContent into a
578
+ * single message item; each ThinkingContent / ToolCall is its own item.
579
+ */
580
+ function buildOutputItems(message: AssistantMessage): OutputItem[] {
581
+ const out: OutputItem[] = [];
582
+ let pendingMessage: MessageOutputItem | null = null;
583
+ const flushMessage = () => {
584
+ if (pendingMessage) {
585
+ out.push(pendingMessage);
586
+ pendingMessage = null;
587
+ }
588
+ };
589
+
590
+ for (const part of message.content) {
591
+ if (part.type === "text") {
592
+ if (!pendingMessage) {
593
+ pendingMessage = {
594
+ type: "message",
595
+ id: makeMsgId(),
596
+ role: "assistant",
597
+ status: "completed",
598
+ content: [],
599
+ };
600
+ }
601
+ pendingMessage.content.push({ type: "output_text", text: part.text, annotations: [] });
602
+ } else if (part.type === "thinking") {
603
+ flushMessage();
604
+ out.push(buildReasoningItem(part));
605
+ } else if (part.type === "toolCall") {
606
+ flushMessage();
607
+ if (part.customWireName) {
608
+ const rawInput = typeof part.arguments?.input === "string" ? (part.arguments.input as string) : "";
609
+ out.push({
610
+ type: "custom_tool_call",
611
+ id: part.thoughtSignature ?? makeCustomCallId(),
612
+ call_id: part.id,
613
+ name: part.customWireName,
614
+ input: rawInput,
615
+ status: "completed",
616
+ });
617
+ } else {
618
+ out.push({
619
+ type: "function_call",
620
+ id: part.thoughtSignature ?? makeFuncCallId(),
621
+ call_id: part.id,
622
+ name: part.name,
623
+ arguments: JSON.stringify(part.arguments ?? {}),
624
+ status: "completed",
625
+ });
626
+ }
627
+ }
628
+ // RedactedThinking / Image are silently dropped — no direct Responses wire representation.
629
+ }
630
+ flushMessage();
631
+ return out;
632
+ }
633
+
634
+ function buildUsage(message: AssistantMessage): Record<string, unknown> {
635
+ const u = message.usage;
636
+ const inputTokens = u.input + u.cacheRead + u.cacheWrite;
637
+ return {
638
+ input_tokens: inputTokens,
639
+ input_tokens_details: { cached_tokens: u.cacheRead },
640
+ output_tokens: u.output,
641
+ output_tokens_details: { reasoning_tokens: u.reasoningTokens ?? 0 },
642
+ total_tokens: inputTokens + u.output,
643
+ };
644
+ }
645
+
646
+ function buildResponseEnvelope(
647
+ message: AssistantMessage,
648
+ requestedModelId: string,
649
+ id: string,
650
+ status: ResponseStatus,
651
+ items: OutputItem[] | [],
652
+ usage: Record<string, unknown> | null,
653
+ ): Record<string, unknown> {
654
+ return {
655
+ id,
656
+ object: "response",
657
+ created_at: Math.floor(message.timestamp / 1000),
658
+ status,
659
+ model: requestedModelId,
660
+ output: items,
661
+ usage,
662
+ ...(status === "incomplete" ? { incomplete_details: { reason: "max_output_tokens" } } : {}),
663
+ ...(status === "failed" ? { error: { message: message.errorMessage ?? "response failed" } } : {}),
664
+ };
665
+ }
666
+
667
+ // ─── encodeResponse (non-streaming) ─────────────────────────────────────────
668
+
669
+ export function encodeResponse(message: AssistantMessage, requestedModelId: string): Record<string, unknown> {
670
+ const items = buildOutputItems(message);
671
+ return buildResponseEnvelope(
672
+ message,
673
+ requestedModelId,
674
+ makeRespId(),
675
+ responseStatusForStopReason(message),
676
+ items,
677
+ buildUsage(message),
678
+ );
679
+ }
680
+
681
+ // ─── encodeStream ───────────────────────────────────────────────────────────
682
+
683
+ interface OpenMessage {
684
+ kind: "message";
685
+ itemId: string;
686
+ outputIndex: number;
687
+ contentIndex: number;
688
+ currentPartText: string;
689
+ content: Array<{ type: "output_text"; text: string; annotations: never[] }>;
690
+ }
691
+ interface OpenReasoning {
692
+ kind: "reasoning";
693
+ itemId: string;
694
+ outputIndex: number;
695
+ reasoningText: string;
696
+ }
697
+ interface OpenFunctionCall {
698
+ kind: "function_call";
699
+ itemId: string;
700
+ outputIndex: number;
701
+ callId: string;
702
+ name: string;
703
+ argsText: string;
704
+ /** Set when the underlying ToolCall is a custom-tool emission. */
705
+ customWireName?: string;
706
+ }
707
+ type OpenItem = OpenMessage | OpenReasoning | OpenFunctionCall;
708
+
709
+ function sseEvent(name: string, data: unknown): string {
710
+ return `event: ${name}\ndata: ${JSON.stringify(data)}\n\n`;
711
+ }
712
+
713
+ export function encodeStream(
714
+ events: AssistantMessageEventStream,
715
+ requestedModelId: string,
716
+ ): ReadableStream<Uint8Array> {
717
+ const encoder = new TextEncoder();
718
+ const responseId = makeRespId();
719
+ let sequenceNumber = 0;
720
+ const seq = () => sequenceNumber++;
721
+
722
+ return new ReadableStream<Uint8Array>({
723
+ async start(controller) {
724
+ const emit = (name: string, data: Record<string, unknown>) => {
725
+ controller.enqueue(encoder.encode(sseEvent(name, { type: name, sequence_number: seq(), ...data })));
726
+ };
727
+ const emitDone = () => controller.enqueue(encoder.encode("data: [DONE]\n\n"));
728
+
729
+ let createdAt = Math.floor(Date.now() / 1000);
730
+ let outputIndex = 0;
731
+ const state: { open: OpenItem | null } = { open: null };
732
+ const finishedItems: OutputItem[] = [];
733
+
734
+ const responseSnapshot = (status: ResponseStatus, output: OutputItem[] | []) => ({
735
+ id: responseId,
736
+ object: "response",
737
+ created_at: createdAt,
738
+ status,
739
+ model: requestedModelId,
740
+ output,
741
+ usage: null,
742
+ });
743
+
744
+ const openMessage = (): OpenMessage => {
745
+ const itemId = makeMsgId();
746
+ const item = {
747
+ type: "message" as const,
748
+ id: itemId,
749
+ status: "in_progress",
750
+ role: "assistant" as const,
751
+ content: [] as Array<{ type: "output_text"; text: string; annotations: never[] }>,
752
+ };
753
+ emit("response.output_item.added", { output_index: outputIndex, item });
754
+ const next: OpenMessage = {
755
+ kind: "message",
756
+ itemId,
757
+ outputIndex,
758
+ contentIndex: 0,
759
+ currentPartText: "",
760
+ content: [],
761
+ };
762
+ state.open = next;
763
+ return next;
764
+ };
765
+
766
+ const openReasoning = (partial: AssistantMessage, contentIndex: number): OpenReasoning => {
767
+ const part = partial.content[contentIndex];
768
+ const itemId = part && part.type === "thinking" ? reasoningItemId(part) : makeReasoningId();
769
+ const item = {
770
+ type: "reasoning" as const,
771
+ id: itemId,
772
+ summary: [] as Array<{ type: "summary_text"; text: string }>,
773
+ };
774
+ emit("response.output_item.added", { output_index: outputIndex, item });
775
+ // Open the summary part. Real OpenAI streams summary text in the
776
+ // canonical `reasoning_summary_*` lifecycle; pi-ai's own decoder
777
+ // reads `summary[].text` from the eventual `output_item.done`.
778
+ emit("response.reasoning_summary_part.added", {
779
+ item_id: itemId,
780
+ output_index: outputIndex,
781
+ summary_index: 0,
782
+ part: { type: "summary_text", text: "" },
783
+ });
784
+ const next: OpenReasoning = { kind: "reasoning", itemId, outputIndex, reasoningText: "" };
785
+ state.open = next;
786
+ return next;
787
+ };
788
+
789
+ const openToolCall = (partial: AssistantMessage, contentIndex: number): OpenFunctionCall => {
790
+ const part = partial.content[contentIndex];
791
+ const tc = part && part.type === "toolCall" ? part : undefined;
792
+ const customWireName: string | undefined =
793
+ tc && typeof tc.customWireName === "string" && tc.customWireName.length > 0
794
+ ? tc.customWireName
795
+ : undefined;
796
+ const isCustom = customWireName !== undefined;
797
+ const itemId = tc?.thoughtSignature ?? (isCustom ? makeCustomCallId() : makeFuncCallId());
798
+ const callId = tc?.id ?? "";
799
+ const name = customWireName ?? tc?.name ?? "";
800
+ const item = isCustom
801
+ ? {
802
+ type: "custom_tool_call" as const,
803
+ id: itemId,
804
+ call_id: callId,
805
+ name,
806
+ input: "",
807
+ status: "in_progress",
808
+ }
809
+ : {
810
+ type: "function_call" as const,
811
+ id: itemId,
812
+ call_id: callId,
813
+ name,
814
+ arguments: "",
815
+ status: "in_progress",
816
+ };
817
+ emit("response.output_item.added", { output_index: outputIndex, item });
818
+ const next: OpenFunctionCall = {
819
+ kind: "function_call",
820
+ itemId,
821
+ outputIndex,
822
+ callId,
823
+ name,
824
+ argsText: "",
825
+ ...(isCustom ? { customWireName } : {}),
826
+ };
827
+ state.open = next;
828
+ return next;
829
+ };
830
+
831
+ const closeOpen = () => {
832
+ if (!state.open) return;
833
+ if (state.open.kind === "message") {
834
+ const item = {
835
+ type: "message",
836
+ id: state.open.itemId,
837
+ status: "completed",
838
+ role: "assistant",
839
+ content: state.open.content,
840
+ };
841
+ emit("response.output_item.done", { output_index: state.open.outputIndex, item });
842
+ finishedItems.push({
843
+ type: "message",
844
+ id: state.open.itemId,
845
+ role: "assistant",
846
+ status: "completed",
847
+ content: state.open.content,
848
+ });
849
+ } else if (state.open.kind === "reasoning") {
850
+ const summary = [{ type: "summary_text" as const, text: state.open.reasoningText ?? "" }];
851
+ const item = {
852
+ type: "reasoning",
853
+ id: state.open.itemId,
854
+ summary,
855
+ };
856
+ emit("response.output_item.done", { output_index: state.open.outputIndex, item });
857
+ finishedItems.push({
858
+ type: "reasoning",
859
+ id: state.open.itemId,
860
+ summary,
861
+ });
862
+ } else {
863
+ const text = state.open.argsText ?? "";
864
+ if (state.open.customWireName) {
865
+ const item = {
866
+ type: "custom_tool_call",
867
+ id: state.open.itemId,
868
+ call_id: state.open.callId ?? "",
869
+ name: state.open.customWireName,
870
+ input: text,
871
+ status: "completed",
872
+ };
873
+ emit("response.output_item.done", { output_index: state.open.outputIndex, item });
874
+ finishedItems.push({
875
+ type: "custom_tool_call",
876
+ id: state.open.itemId,
877
+ call_id: state.open.callId ?? "",
878
+ name: state.open.customWireName,
879
+ input: text,
880
+ status: "completed",
881
+ });
882
+ } else {
883
+ const item = {
884
+ type: "function_call",
885
+ id: state.open.itemId,
886
+ call_id: state.open.callId ?? "",
887
+ name: state.open.name ?? "",
888
+ arguments: text,
889
+ status: "completed",
890
+ };
891
+ emit("response.output_item.done", { output_index: state.open.outputIndex, item });
892
+ finishedItems.push({
893
+ type: "function_call",
894
+ id: state.open.itemId,
895
+ call_id: state.open.callId ?? "",
896
+ name: state.open.name ?? "",
897
+ arguments: text,
898
+ status: "completed",
899
+ });
900
+ }
901
+ }
902
+ outputIndex++;
903
+ state.open = null;
904
+ };
905
+
906
+ try {
907
+ let finalMessage: AssistantMessage | null = null;
908
+ let failureMessage: AssistantMessage | null = null;
909
+
910
+ for await (const ev of events) {
911
+ switch (ev.type) {
912
+ case "start": {
913
+ createdAt = Math.floor((ev.partial.timestamp || Date.now()) / 1000);
914
+ // response.created — initial envelope.
915
+ controller.enqueue(
916
+ encoder.encode(
917
+ sseEvent("response.created", {
918
+ type: "response.created",
919
+ sequence_number: seq(),
920
+ response: responseSnapshot("in_progress", []),
921
+ }),
922
+ ),
923
+ );
924
+ // response.in_progress — mirrors real OpenAI; some clients gate
925
+ // on it before reading items.
926
+ controller.enqueue(
927
+ encoder.encode(
928
+ sseEvent("response.in_progress", {
929
+ type: "response.in_progress",
930
+ sequence_number: seq(),
931
+ response: responseSnapshot("in_progress", []),
932
+ }),
933
+ ),
934
+ );
935
+ break;
936
+ }
937
+ case "text_start": {
938
+ let cur: OpenMessage;
939
+ if (state.open && state.open.kind === "message") {
940
+ // continue same message item, new content part
941
+ cur = state.open;
942
+ cur.currentPartText = "";
943
+ } else {
944
+ if (state.open) closeOpen();
945
+ cur = openMessage();
946
+ }
947
+ const part = { type: "output_text", text: "", annotations: [] as never[] };
948
+ emit("response.content_part.added", {
949
+ item_id: cur.itemId,
950
+ output_index: cur.outputIndex,
951
+ content_index: cur.contentIndex,
952
+ part,
953
+ });
954
+ break;
955
+ }
956
+ case "text_delta": {
957
+ if (!state.open || state.open.kind !== "message") break;
958
+ const cur: OpenMessage = state.open;
959
+ cur.currentPartText += ev.delta;
960
+ emit("response.output_text.delta", {
961
+ item_id: cur.itemId,
962
+ output_index: cur.outputIndex,
963
+ content_index: cur.contentIndex,
964
+ delta: ev.delta,
965
+ logprobs: [],
966
+ });
967
+ // TODO: when pi-ai surfaces output_text annotations
968
+ // (web_search citations, …), emit
969
+ // `response.output_text.annotation.added` here.
970
+ break;
971
+ }
972
+ case "text_end": {
973
+ if (!state.open || state.open.kind !== "message") break;
974
+ const cur: OpenMessage = state.open;
975
+ const text = ev.content ?? cur.currentPartText;
976
+ emit("response.output_text.done", {
977
+ item_id: cur.itemId,
978
+ output_index: cur.outputIndex,
979
+ content_index: cur.contentIndex,
980
+ text,
981
+ logprobs: [],
982
+ });
983
+ cur.content.push({ type: "output_text", text, annotations: [] });
984
+ emit("response.content_part.done", {
985
+ item_id: cur.itemId,
986
+ output_index: cur.outputIndex,
987
+ content_index: cur.contentIndex,
988
+ part: { type: "output_text", text, annotations: [] },
989
+ });
990
+ cur.contentIndex += 1;
991
+ cur.currentPartText = "";
992
+ break;
993
+ }
994
+ case "thinking_start": {
995
+ if (state.open) closeOpen();
996
+ openReasoning(ev.partial, ev.contentIndex);
997
+ break;
998
+ }
999
+ case "thinking_delta": {
1000
+ if (!state.open || state.open.kind !== "reasoning") break;
1001
+ const cur: OpenReasoning = state.open;
1002
+ cur.reasoningText += ev.delta;
1003
+ emit("response.reasoning_summary_text.delta", {
1004
+ item_id: cur.itemId,
1005
+ output_index: cur.outputIndex,
1006
+ summary_index: 0,
1007
+ delta: ev.delta,
1008
+ });
1009
+ break;
1010
+ }
1011
+ case "thinking_end": {
1012
+ if (!state.open || state.open.kind !== "reasoning") break;
1013
+ const cur: OpenReasoning = state.open;
1014
+ const text = ev.content ?? cur.reasoningText;
1015
+ cur.reasoningText = text;
1016
+ emit("response.reasoning_summary_text.done", {
1017
+ item_id: cur.itemId,
1018
+ output_index: cur.outputIndex,
1019
+ summary_index: 0,
1020
+ text,
1021
+ });
1022
+ emit("response.reasoning_summary_part.done", {
1023
+ item_id: cur.itemId,
1024
+ output_index: cur.outputIndex,
1025
+ summary_index: 0,
1026
+ part: { type: "summary_text", text },
1027
+ });
1028
+ closeOpen();
1029
+ break;
1030
+ }
1031
+ case "toolcall_start": {
1032
+ if (state.open) closeOpen();
1033
+ openToolCall(ev.partial, ev.contentIndex);
1034
+ break;
1035
+ }
1036
+ case "toolcall_delta": {
1037
+ if (!state.open || state.open.kind !== "function_call") break;
1038
+ const cur: OpenFunctionCall = state.open;
1039
+ cur.argsText += ev.delta;
1040
+ if (cur.customWireName) {
1041
+ emit("response.custom_tool_call_input.delta", {
1042
+ item_id: cur.itemId,
1043
+ output_index: cur.outputIndex,
1044
+ delta: ev.delta,
1045
+ });
1046
+ } else {
1047
+ emit("response.function_call_arguments.delta", {
1048
+ item_id: cur.itemId,
1049
+ output_index: cur.outputIndex,
1050
+ delta: ev.delta,
1051
+ });
1052
+ }
1053
+ break;
1054
+ }
1055
+ case "toolcall_end": {
1056
+ if (!state.open || state.open.kind !== "function_call") break;
1057
+ const cur: OpenFunctionCall = state.open;
1058
+ // Promote possibly-late info from the canonical ToolCall.
1059
+ const tc = ev.toolCall;
1060
+ if (tc.customWireName && !cur.customWireName) cur.customWireName = tc.customWireName;
1061
+ if (tc.thoughtSignature) cur.itemId = tc.thoughtSignature;
1062
+ cur.callId = tc.id;
1063
+ cur.name = cur.customWireName ?? tc.name;
1064
+ if (cur.customWireName) {
1065
+ // Custom tool: raw input string. Streamed deltas accumulated
1066
+ // the wire-level body; fall back to `arguments.input` from
1067
+ // the finalized ToolCall when nothing streamed (rare).
1068
+ const rawInput =
1069
+ cur.argsText ||
1070
+ (typeof tc.arguments?.input === "string" ? (tc.arguments.input as string) : "");
1071
+ cur.argsText = rawInput;
1072
+ emit("response.custom_tool_call_input.done", {
1073
+ item_id: cur.itemId,
1074
+ output_index: cur.outputIndex,
1075
+ input: rawInput,
1076
+ name: cur.name,
1077
+ });
1078
+ } else {
1079
+ // Standard JSON tool: arguments object on the gjc side, the
1080
+ // wire wants the JSON string the model emitted (= streamed deltas).
1081
+ const argsJson = cur.argsText || JSON.stringify(tc.arguments ?? {});
1082
+ cur.argsText = argsJson;
1083
+ emit("response.function_call_arguments.done", {
1084
+ item_id: cur.itemId,
1085
+ output_index: cur.outputIndex,
1086
+ arguments: argsJson,
1087
+ name: cur.name,
1088
+ });
1089
+ }
1090
+ closeOpen();
1091
+ break;
1092
+ }
1093
+ case "done": {
1094
+ finalMessage = ev.message;
1095
+ break;
1096
+ }
1097
+ case "error": {
1098
+ failureMessage = ev.error;
1099
+ break;
1100
+ }
1101
+ }
1102
+ }
1103
+
1104
+ if (failureMessage) {
1105
+ if (state.open) closeOpen();
1106
+ controller.enqueue(
1107
+ encoder.encode(
1108
+ sseEvent("response.failed", {
1109
+ type: "response.failed",
1110
+ sequence_number: seq(),
1111
+ response: {
1112
+ ...responseSnapshot("failed", finishedItems),
1113
+ error: { message: failureMessage.errorMessage ?? "stream failed" },
1114
+ },
1115
+ }),
1116
+ ),
1117
+ );
1118
+ emitDone();
1119
+ controller.close();
1120
+ return;
1121
+ }
1122
+
1123
+ if (state.open) closeOpen();
1124
+ const message = finalMessage ?? ((await events.result().catch(() => null)) as AssistantMessage | null);
1125
+
1126
+ // Build the canonical output from the final message so non-streaming
1127
+ // readers see the exact same shape they'd get from encodeResponse().
1128
+ const items = message ? buildOutputItems(message) : finishedItems;
1129
+ const usage = message ? buildUsage(message) : null;
1130
+ const status = message ? responseStatusForStopReason(message) : "completed";
1131
+ const terminalEvent =
1132
+ status === "incomplete"
1133
+ ? "response.incomplete"
1134
+ : status === "failed"
1135
+ ? "response.failed"
1136
+ : "response.completed";
1137
+ controller.enqueue(
1138
+ encoder.encode(
1139
+ sseEvent(terminalEvent, {
1140
+ type: terminalEvent,
1141
+ sequence_number: seq(),
1142
+ response: {
1143
+ id: responseId,
1144
+ object: "response",
1145
+ created_at: createdAt,
1146
+ status,
1147
+ model: requestedModelId,
1148
+ output: items,
1149
+ usage,
1150
+ ...(status === "incomplete" ? { incomplete_details: { reason: "max_output_tokens" } } : {}),
1151
+ ...(status === "failed"
1152
+ ? { error: { message: message?.errorMessage ?? "response failed" } }
1153
+ : {}),
1154
+ },
1155
+ }),
1156
+ ),
1157
+ );
1158
+ emitDone();
1159
+ controller.close();
1160
+ } catch (err) {
1161
+ controller.enqueue(
1162
+ encoder.encode(
1163
+ sseEvent("response.failed", {
1164
+ type: "response.failed",
1165
+ sequence_number: seq(),
1166
+ response: {
1167
+ id: responseId,
1168
+ object: "response",
1169
+ created_at: Math.floor(Date.now() / 1000),
1170
+ status: "failed",
1171
+ model: requestedModelId,
1172
+ output: [],
1173
+ error: { message: err instanceof Error ? err.message : String(err) },
1174
+ },
1175
+ }),
1176
+ ),
1177
+ );
1178
+ emitDone();
1179
+ controller.close();
1180
+ }
1181
+ },
1182
+ });
1183
+ }