@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,224 @@
1
+ import { UNK_CONTEXT_WINDOW, UNK_MAX_TOKENS } from "@gajae-code/ai";
2
+ import * as z from "zod/v4";
3
+ import type { Api, Model, Provider } from "../../types";
4
+
5
+ const MODELS_PATH = "/models";
6
+
7
+ /**
8
+ * Minimal OpenAI-style model entry shape consumed by discovery.
9
+ *
10
+ * Providers may return additional fields; this type only captures
11
+ * fields that are useful for generic normalization.
12
+ */
13
+ export interface OpenAICompatibleModelRecord {
14
+ id?: unknown;
15
+ name?: unknown;
16
+ object?: unknown;
17
+ owned_by?: unknown;
18
+ [key: string]: unknown;
19
+ }
20
+
21
+ /**
22
+ * Tolerant envelope for OpenAI-compatible `/models` responses.
23
+ *
24
+ * Common providers return `{ data: [...] }`, but variants such as
25
+ * `{ models: [...] }`, `{ result: [...] }`, or direct arrays are also
26
+ * accepted during extraction.
27
+ */
28
+ export interface OpenAICompatibleModelsEnvelope {
29
+ data?: unknown;
30
+ models?: unknown;
31
+ result?: unknown;
32
+ items?: unknown;
33
+ [key: string]: unknown;
34
+ }
35
+
36
+ const openAICompatibleModelRecordSchema = z
37
+ .object({
38
+ id: z.string().min(1),
39
+ name: z.string().optional().nullable(),
40
+ object: z.unknown().optional(),
41
+ owned_by: z.unknown().optional(),
42
+ })
43
+ .loose();
44
+
45
+ const openAICompatibleModelsEnvelopeSchema = z
46
+ .object({
47
+ data: z.unknown().optional(),
48
+ models: z.unknown().optional(),
49
+ result: z.unknown().optional(),
50
+ items: z.unknown().optional(),
51
+ })
52
+ .loose();
53
+
54
+ const openAICompatibleModelsPayloadSchema = z.union([z.array(z.unknown()), openAICompatibleModelsEnvelopeSchema]);
55
+
56
+ type ParsedOpenAICompatibleModelRecord = z.infer<typeof openAICompatibleModelRecordSchema>;
57
+
58
+ /**
59
+ * Context passed to custom OpenAI-compatible model mappers.
60
+ */
61
+ export interface OpenAICompatibleModelMapperContext<TApi extends Api> {
62
+ api: TApi;
63
+ provider: Provider;
64
+ baseUrl: string;
65
+ }
66
+
67
+ /**
68
+ * Options for fetching and normalizing OpenAI-compatible `/models` catalogs.
69
+ */
70
+ export interface FetchOpenAICompatibleModelsOptions<TApi extends Api> {
71
+ /** API type assigned to normalized models. */
72
+ api: TApi;
73
+ /** Provider id assigned to normalized models. */
74
+ provider: Provider;
75
+ /** Provider base URL used for both fetch and normalized model records. */
76
+ baseUrl: string;
77
+ /** Optional bearer token for Authorization header. */
78
+ apiKey?: string;
79
+ /** Additional request headers. */
80
+ headers?: Record<string, string>;
81
+ /** Optional AbortSignal for request cancellation. */
82
+ signal?: AbortSignal;
83
+ /** Optional fetch implementation override for testing/custom runtimes. */
84
+ fetch?: typeof globalThis.fetch;
85
+ /**
86
+ * Optional post-normalization filter.
87
+ * Return false to skip a model.
88
+ */
89
+ filterModel?: (entry: OpenAICompatibleModelRecord, model: Model<TApi>) => boolean;
90
+ /**
91
+ * Optional mapper override for provider-specific quirks.
92
+ * Return null to skip a model.
93
+ */
94
+ mapModel?: (
95
+ entry: OpenAICompatibleModelRecord,
96
+ defaults: Model<TApi>,
97
+ context: OpenAICompatibleModelMapperContext<TApi>,
98
+ ) => Model<TApi> | null;
99
+ }
100
+
101
+ /**
102
+ * Fetches and normalizes an OpenAI-compatible `/models` catalog.
103
+ *
104
+ * Returns `null` on transport/protocol failures.
105
+ * Returns `[]` only when the endpoint responds successfully with no usable models.
106
+ */
107
+ export async function fetchOpenAICompatibleModels<TApi extends Api>(
108
+ options: FetchOpenAICompatibleModelsOptions<TApi>,
109
+ ): Promise<Model<TApi>[] | null> {
110
+ const baseUrl = normalizeBaseUrl(options.baseUrl);
111
+ if (!baseUrl) {
112
+ return null;
113
+ }
114
+
115
+ const requestHeaders: Record<string, string> = {
116
+ Accept: "application/json",
117
+ ...options.headers,
118
+ };
119
+ if (options.apiKey) {
120
+ requestHeaders.Authorization = `Bearer ${options.apiKey}`;
121
+ }
122
+
123
+ const fetchImpl = options.fetch ?? globalThis.fetch;
124
+ let response: Response;
125
+ try {
126
+ response = await fetchImpl(`${baseUrl}${MODELS_PATH}`, {
127
+ method: "GET",
128
+ headers: requestHeaders,
129
+ signal: options.signal,
130
+ });
131
+ } catch {
132
+ return null;
133
+ }
134
+
135
+ if (!response.ok) {
136
+ return null;
137
+ }
138
+
139
+ let payload: unknown;
140
+ try {
141
+ payload = await response.json();
142
+ } catch {
143
+ return null;
144
+ }
145
+
146
+ const entries = extractModelEntries(payload);
147
+ if (entries === null) {
148
+ return null;
149
+ }
150
+
151
+ const context: OpenAICompatibleModelMapperContext<TApi> = {
152
+ api: options.api,
153
+ provider: options.provider,
154
+ baseUrl,
155
+ };
156
+
157
+ const deduped = new Map<string, Model<TApi>>();
158
+ for (const entry of entries) {
159
+ const defaults: Model<TApi> = {
160
+ id: entry.id,
161
+ name: typeof entry.name === "string" && entry.name.length > 0 ? entry.name : entry.id,
162
+ api: options.api,
163
+ provider: options.provider,
164
+ baseUrl,
165
+ reasoning: false,
166
+ input: ["text"],
167
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
168
+ contextWindow: UNK_CONTEXT_WINDOW,
169
+ maxTokens: UNK_MAX_TOKENS,
170
+ };
171
+
172
+ const mapped = options.mapModel?.(entry, defaults, context) ?? defaults;
173
+ if (!mapped || typeof mapped.id !== "string" || mapped.id.length === 0) {
174
+ continue;
175
+ }
176
+ if (options.filterModel && !options.filterModel(entry, mapped)) {
177
+ continue;
178
+ }
179
+ deduped.set(mapped.id, mapped);
180
+ }
181
+
182
+ return Array.from(deduped.values()).sort((left, right) => left.id.localeCompare(right.id));
183
+ }
184
+
185
+ function normalizeBaseUrl(baseUrl: string): string {
186
+ const trimmed = baseUrl.trim();
187
+ if (!trimmed) {
188
+ return "";
189
+ }
190
+ return trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
191
+ }
192
+
193
+ function extractModelEntries(payload: unknown): ParsedOpenAICompatibleModelRecord[] | null {
194
+ return extractModelEntriesFromNode(payload);
195
+ }
196
+
197
+ function extractModelEntriesFromNode(node: unknown): ParsedOpenAICompatibleModelRecord[] | null {
198
+ const parsedPayload = openAICompatibleModelsPayloadSchema.safeParse(node);
199
+ if (!parsedPayload.success) {
200
+ return null;
201
+ }
202
+ if (Array.isArray(parsedPayload.data)) {
203
+ const parsedEntries = parsedPayload.data
204
+ .map(entry => openAICompatibleModelRecordSchema.safeParse(entry))
205
+ .flatMap(entry => (entry.success ? [entry.data] : []));
206
+ return parsedEntries;
207
+ }
208
+ for (const candidate of [
209
+ parsedPayload.data.data,
210
+ parsedPayload.data.models,
211
+ parsedPayload.data.result,
212
+ parsedPayload.data.items,
213
+ ]) {
214
+ if (candidate === undefined) {
215
+ continue;
216
+ }
217
+ const nested = extractModelEntriesFromNode(candidate);
218
+ if (nested !== null) {
219
+ return nested;
220
+ }
221
+ }
222
+
223
+ return null;
224
+ }
@@ -0,0 +1,142 @@
1
+ import type { AssistantMessage, AssistantMessageEvent } from "../types";
2
+
3
+ // Generic event stream class for async iteration
4
+ export class EventStream<T, R = T> implements AsyncIterable<T> {
5
+ queue: T[] = [];
6
+ waiting: Array<{ resolve: (value: IteratorResult<T>) => void; reject: (err: unknown) => void }> = [];
7
+ done = false;
8
+ #failed = false;
9
+ #error: unknown = undefined;
10
+ finalResultPromise: Promise<R>;
11
+ resolveFinalResult!: (result: R) => void;
12
+ rejectFinalResult!: (err: unknown) => void;
13
+ isComplete: (event: T) => boolean;
14
+ extractResult: (event: T) => R;
15
+
16
+ constructor(isComplete: (event: T) => boolean, extractResult: (event: T) => R) {
17
+ const { promise, resolve, reject } = Promise.withResolvers<R>();
18
+ // Prevent an unhandled rejection when fail() is called but nobody awaits result().
19
+ // Callers who do await result() still receive the rejection normally.
20
+ promise.catch(() => {});
21
+ this.finalResultPromise = promise;
22
+ this.resolveFinalResult = resolve;
23
+ this.rejectFinalResult = reject;
24
+ this.isComplete = isComplete;
25
+ this.extractResult = extractResult;
26
+ }
27
+
28
+ push(event: T): void {
29
+ if (this.done) return;
30
+
31
+ if (this.isComplete(event)) {
32
+ this.done = true;
33
+ this.resolveFinalResult(this.extractResult(event));
34
+ }
35
+
36
+ // Deliver to waiting consumer or queue it
37
+ const waiter = this.waiting.shift();
38
+ if (waiter) {
39
+ waiter.resolve({ value: event, done: false });
40
+ } else {
41
+ this.queue.push(event);
42
+ }
43
+ }
44
+
45
+ deliver(event: T): void {
46
+ const waiter = this.waiting.shift();
47
+ if (waiter) {
48
+ waiter.resolve({ value: event, done: false });
49
+ } else {
50
+ this.queue.push(event);
51
+ }
52
+ }
53
+
54
+ end(result?: R): void {
55
+ this.done = true;
56
+ if (result !== undefined) {
57
+ this.resolveFinalResult(result);
58
+ }
59
+ // Notify all waiting consumers that we're done
60
+ while (this.waiting.length > 0) {
61
+ const waiter = this.waiting.shift()!;
62
+ waiter.resolve({ value: undefined as any, done: true });
63
+ }
64
+ }
65
+
66
+ endWaiting(): void {
67
+ while (this.waiting.length > 0) {
68
+ const waiter = this.waiting.shift()!;
69
+ waiter.resolve({ value: undefined as any, done: true });
70
+ }
71
+ }
72
+
73
+ fail(err: unknown): void {
74
+ if (this.done) return;
75
+ this.done = true;
76
+ this.#failed = true;
77
+ this.#error = err;
78
+ this.rejectFinalResult(err);
79
+ while (this.waiting.length > 0) {
80
+ const waiter = this.waiting.shift()!;
81
+ waiter.reject(err);
82
+ }
83
+ }
84
+
85
+ async *[Symbol.asyncIterator](): AsyncIterator<T> {
86
+ while (true) {
87
+ if (this.queue.length > 0) {
88
+ yield this.queue.shift()!;
89
+ } else if (this.#failed) {
90
+ throw this.#error;
91
+ } else if (this.done) {
92
+ return;
93
+ } else {
94
+ const result = await new Promise<IteratorResult<T>>((resolve, reject) =>
95
+ this.waiting.push({ resolve, reject }),
96
+ );
97
+ if (result.done) return;
98
+ yield result.value;
99
+ }
100
+ }
101
+ }
102
+
103
+ result(): Promise<R> {
104
+ return this.finalResultPromise;
105
+ }
106
+ }
107
+
108
+ export class AssistantMessageEventStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
109
+ constructor() {
110
+ super(
111
+ event => event.type === "done" || event.type === "error",
112
+ event => {
113
+ if (event.type === "done") {
114
+ return event.message;
115
+ } else if (event.type === "error") {
116
+ return event.error;
117
+ }
118
+ throw new Error("Unexpected event type for final result");
119
+ },
120
+ );
121
+ }
122
+
123
+ override push(event: AssistantMessageEvent): void {
124
+ if (this.done) return;
125
+
126
+ // Completion resolves the final result and still emits the terminal event.
127
+ if (this.isComplete(event)) {
128
+ this.done = true;
129
+ this.resolveFinalResult(this.extractResult(event));
130
+ }
131
+
132
+ this.deliver(event);
133
+ }
134
+
135
+ override end(result?: AssistantMessage): void {
136
+ this.done = true;
137
+ if (result !== undefined) {
138
+ this.resolveFinalResult(result);
139
+ }
140
+ this.endWaiting();
141
+ }
142
+ }
@@ -0,0 +1,30 @@
1
+ const FIREWORKS_WIRE_PREFIX = "accounts/fireworks/models/";
2
+ const FIREPASS_WIRE_PREFIX = "accounts/fireworks/routers/";
3
+ const VERSION_SEPARATOR_PATTERN = /(?<=\d)p(?=\d)/g;
4
+ const VERSION_DOT_PATTERN = /(?<=\d)\.(?=\d)/g;
5
+
6
+ export function toFireworksPublicModelId(modelId: string): string {
7
+ const stripped = modelId.startsWith(FIREWORKS_WIRE_PREFIX) ? modelId.slice(FIREWORKS_WIRE_PREFIX.length) : modelId;
8
+ return stripped.replace(VERSION_SEPARATOR_PATTERN, ".");
9
+ }
10
+
11
+ export function toFireworksWireModelId(modelId: string): string {
12
+ const stripped = modelId.startsWith(FIREWORKS_WIRE_PREFIX) ? modelId.slice(FIREWORKS_WIRE_PREFIX.length) : modelId;
13
+ return `${FIREWORKS_WIRE_PREFIX}${stripped.replace(VERSION_DOT_PATTERN, "p")}`;
14
+ }
15
+
16
+ /**
17
+ * Fire Pass exposes its Kimi K2.6 Turbo subscription through a dedicated router
18
+ * endpoint at `accounts/fireworks/routers/<id>` rather than the `models/` namespace.
19
+ * We keep a friendly public id (e.g. `kimi-k2.6-turbo`) in the catalog and translate
20
+ * to the wire form (`accounts/fireworks/routers/kimi-k2p6-turbo`) at request time.
21
+ */
22
+ export function toFirepassPublicModelId(modelId: string): string {
23
+ const stripped = modelId.startsWith(FIREPASS_WIRE_PREFIX) ? modelId.slice(FIREPASS_WIRE_PREFIX.length) : modelId;
24
+ return stripped.replace(VERSION_SEPARATOR_PATTERN, ".");
25
+ }
26
+
27
+ export function toFirepassWireModelId(modelId: string): string {
28
+ const stripped = modelId.startsWith(FIREPASS_WIRE_PREFIX) ? modelId.slice(FIREPASS_WIRE_PREFIX.length) : modelId;
29
+ return `${FIREPASS_WIRE_PREFIX}${stripped.replace(VERSION_DOT_PATTERN, "p")}`;
30
+ }
@@ -0,0 +1,8 @@
1
+ import { $env } from "@gajae-code/utils";
2
+
3
+ export function isFoundryEnabled(): boolean {
4
+ const value = $env.CLAUDE_CODE_USE_FOUNDRY;
5
+ if (!value) return false;
6
+ const normalized = value.trim().toLowerCase();
7
+ return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
8
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Patch `globalThis.fetch` to advertise HTTP/2 in TLS ALPN, with transparent
3
+ * HTTP/1.1 fallback when the server doesn't negotiate `h2`.
4
+ *
5
+ * Bun's HTTP/2 client is gated on `BUN_FEATURE_FLAG_EXPERIMENTAL_HTTP2_CLIENT`,
6
+ * read by the native runtime before any JS executes; assigning to
7
+ * `process.env` from inside JS is a no-op. Per-request `protocol: "http2"`
8
+ * activates h2 over TLS ALPN and rejects with `error.code === "HTTP2Unsupported"`
9
+ * if the server picks anything else, so we catch and retry without the hint.
10
+ *
11
+ * Some HTTPS endpoints (e.g. corporate API gateways behind reverse proxies)
12
+ * advertise h2 via ALPN but then refuse or reset the connection at the HTTP/2
13
+ * framing layer. Bun surfaces these as `ConnectionRefused`, `ConnectionReset`,
14
+ * or `ConnectionClosed` rather than `HTTP2Unsupported`, so we treat those
15
+ * codes as h2-fallback triggers as well.
16
+ *
17
+ * Bun negotiates h2 via ALPN over TLS only (no h2c), so plain `http://` URLs
18
+ * skip the attempt entirely — avoids the throw/retry round-trip for localhost.
19
+ *
20
+ * Idempotent.
21
+ */
22
+
23
+ const installed: unique symbol = Symbol.for("gajae-code.h2fetch.installed");
24
+
25
+ interface PatchedFetch {
26
+ [installed]?: true;
27
+ }
28
+
29
+ export function installH2Fetch(): void {
30
+ const original = globalThis.fetch as typeof fetch & PatchedFetch;
31
+ if (original[installed]) return;
32
+
33
+ /** Error codes that indicate h2 negotiation/transport failure (not an application error). */
34
+ const h2FallbackCodes: ReadonlySet<string> = new Set([
35
+ "HTTP2Unsupported", // Server selected h1 in ALPN
36
+ "ConnectionRefused", // Server refused the h2 connection
37
+ "ConnectionReset", // Server reset during h2 handshake
38
+ "ConnectionClosed", // Server closed before h2 response
39
+ ]);
40
+ const wrapper = async function h2fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> {
41
+ if (!isHttps(input)) return original(input, init);
42
+ try {
43
+ return await original(input, { ...init, protocol: "http2" });
44
+ } catch (err) {
45
+ if (!h2FallbackCodes.has((err as { code?: string }).code ?? "")) throw err;
46
+ return original(input, init);
47
+ }
48
+ } as typeof fetch & PatchedFetch;
49
+
50
+ // Preserve `fetch.preconnect` and any other statics SDK code might poke at.
51
+ Object.assign(wrapper, original);
52
+ wrapper[installed] = true;
53
+ globalThis.fetch = wrapper;
54
+ }
55
+
56
+ function isHttps(input: string | URL | Request): boolean {
57
+ if (typeof input === "string") return input.startsWith("https:");
58
+ if (input instanceof URL) return input.protocol === "https:";
59
+ return input.url.startsWith("https:");
60
+ }
@@ -0,0 +1,176 @@
1
+ import * as path from "node:path";
2
+ import { extractHttpStatusFromError, getLogsDir } from "@gajae-code/utils";
3
+ import { isCopilotTransientModelError } from "./retry.js";
4
+ import { formatErrorMessageWithRetryAfter } from "./retry-after.js";
5
+
6
+ export type RawHttpRequestDump = {
7
+ provider: string;
8
+ api: string;
9
+ model: string;
10
+ method?: string;
11
+ url?: string;
12
+ headers?: Record<string, string>;
13
+ body?: unknown;
14
+ };
15
+
16
+ export type CapturedHttpErrorResponse = {
17
+ status: number;
18
+ headers?: Headers;
19
+ bodyText?: string;
20
+ bodyJson?: unknown;
21
+ };
22
+
23
+ type ErrorWithStatus = {
24
+ status?: unknown;
25
+ };
26
+
27
+ const SENSITIVE_HEADERS = ["authorization", "x-api-key", "api-key", "cookie", "set-cookie", "proxy-authorization"];
28
+
29
+ export async function appendRawHttpRequestDumpFor400(
30
+ message: string,
31
+ error: unknown,
32
+ dump: RawHttpRequestDump | undefined,
33
+ ): Promise<string> {
34
+ if (!dump || extractHttpStatusFromError(error) !== 400) {
35
+ return message;
36
+ }
37
+
38
+ const sanitizedDump = sanitizeDump(dump);
39
+ const fileName = `${Date.now()}-${Bun.hash(JSON.stringify(sanitizedDump)).toString(36)}.json`;
40
+ const filePath = path.join(getLogsDir(), "http-400-requests", fileName);
41
+
42
+ try {
43
+ await Bun.write(filePath, `${JSON.stringify(sanitizedDump, null, 2)}\n`);
44
+ return `${message}\nraw-http-request=${filePath}`;
45
+ } catch (writeError) {
46
+ const writeMessage = writeError instanceof Error ? writeError.message : String(writeError);
47
+ return `${message}\nraw-http-request-save-failed=${writeMessage}`;
48
+ }
49
+ }
50
+
51
+ export async function finalizeErrorMessage(
52
+ error: unknown,
53
+ rawRequestDump: RawHttpRequestDump | undefined,
54
+ capturedErrorResponse?: CapturedHttpErrorResponse,
55
+ ): Promise<string> {
56
+ let message = formatErrorMessageWithRetryAfter(error, capturedErrorResponse?.headers);
57
+ const capturedMessage = formatCapturedHttpError(capturedErrorResponse);
58
+ if (capturedMessage) {
59
+ if (/\bstatus code\s*\(no body\)/i.test(message)) {
60
+ message = `${capturedErrorResponse?.status ?? "HTTP"} status code: ${capturedMessage}`;
61
+ } else if (!message.includes(capturedMessage)) {
62
+ message = `${message}\n${capturedMessage}`;
63
+ }
64
+ }
65
+ return appendRawHttpRequestDumpFor400(message, error, rawRequestDump);
66
+ }
67
+
68
+ export function withHttpStatus(error: unknown, status: number): Error {
69
+ const wrapped = error instanceof Error ? error : new Error(String(error));
70
+ (wrapped as ErrorWithStatus).status = status;
71
+ return wrapped;
72
+ }
73
+
74
+ /**
75
+ * Rewrite error message for GitHub Copilot request failures.
76
+ * Must run AFTER finalizeErrorMessage since it replaces the message entirely.
77
+ *
78
+ * 400 `model_not_supported` = Copilot routing rollout gap for our OAuth client.
79
+ * A preview model (gpt-5.3-OpenAI code backend, gpt-5.4*, ...) flaps between 200 and
80
+ * 400 because only some of Copilot's backends have the model. After the
81
+ * in-request retry exhausts, surface guidance rather than the raw error.
82
+ * 401 = token invalid/expired → credential removal is safe, prompt re-login.
83
+ * 403 = token valid but access denied (plan, model policy, org restriction) →
84
+ * do NOT reuse the auth-failed string (which triggers credential removal).
85
+ */
86
+ export function rewriteCopilotError(errorMessage: string, error: unknown, provider: string): string {
87
+ if (provider !== "github-copilot") return errorMessage;
88
+ const status = extractHttpStatusFromError(error);
89
+ if (status === 401) {
90
+ return `GitHub Copilot authentication failed (HTTP 401). Your token may have been revoked. Please re-login with /login github-copilot`;
91
+ }
92
+ if (status === 403) {
93
+ return `GitHub Copilot access denied (HTTP 403). Your account may not have access to this model or feature. Check your Copilot plan or model policy settings.`;
94
+ }
95
+ if (isCopilotTransientModelError(error)) {
96
+ return `GitHub Copilot rejected this model (HTTP 400 model_not_supported) after retries. This is a known intermittent rollout gap for preview models on OAuth clients other than VS Code. Try again in a few seconds, switch to a GA model (gpt-5-mini, gpt-5.2), or run this model from VS Code.`;
97
+ }
98
+ return errorMessage;
99
+ }
100
+
101
+ function sanitizeDump(dump: RawHttpRequestDump): RawHttpRequestDump {
102
+ return {
103
+ ...dump,
104
+ headers: redactHeaders(dump.headers),
105
+ };
106
+ }
107
+
108
+ function redactHeaders(headers: Record<string, string> | undefined): Record<string, string> | undefined {
109
+ if (!headers) {
110
+ return undefined;
111
+ }
112
+
113
+ const redacted: Record<string, string> = {};
114
+ for (const [key, value] of Object.entries(headers)) {
115
+ if (SENSITIVE_HEADERS.includes(key.toLowerCase())) {
116
+ redacted[key] = "[redacted]";
117
+ continue;
118
+ }
119
+ redacted[key] = value;
120
+ }
121
+ return redacted;
122
+ }
123
+
124
+ function formatCapturedHttpError(captured: CapturedHttpErrorResponse | undefined): string | undefined {
125
+ if (!captured) return undefined;
126
+ const bodyText = captured.bodyText?.trim();
127
+ if (!bodyText) return undefined;
128
+ const payload = parseCapturedErrorPayload(captured);
129
+ if (!payload) return bodyText;
130
+
131
+ const errorPayload = getObjectProperty(payload, "error") ?? payload;
132
+ // {"error": "string"} — the error value is a plain string, not a nested object.
133
+ // Fall back to it when the structured fields ("message", etc.) are absent.
134
+ const stringError = errorPayload === payload ? getStringProperty(payload, "error") : undefined;
135
+ const message =
136
+ getStringProperty(errorPayload, "message") ?? getStringProperty(payload, "message") ?? stringError ?? bodyText;
137
+ const extras = [
138
+ getStringProperty(errorPayload, "type") ?? getStringProperty(payload, "type"),
139
+ getStringProperty(errorPayload, "param") ?? getStringProperty(payload, "param"),
140
+ getStringProperty(errorPayload, "code") ?? getStringProperty(payload, "code"),
141
+ ]
142
+ .filter(Boolean)
143
+ .map((value, index) => {
144
+ if (index === 0) return `type=${value}`;
145
+ if (index === 1) return `param=${value}`;
146
+ return `code=${value}`;
147
+ });
148
+ return extras.length > 0 ? `${message} (${extras.join(" ")})` : message;
149
+ }
150
+
151
+ function parseCapturedErrorPayload(captured: CapturedHttpErrorResponse): Record<string, unknown> | undefined {
152
+ if (isObject(captured.bodyJson)) {
153
+ return captured.bodyJson;
154
+ }
155
+ if (!captured.bodyText) return undefined;
156
+ try {
157
+ const parsed = JSON.parse(captured.bodyText);
158
+ return isObject(parsed) ? parsed : undefined;
159
+ } catch {
160
+ return undefined;
161
+ }
162
+ }
163
+
164
+ function getObjectProperty(value: Record<string, unknown>, key: string): Record<string, unknown> | undefined {
165
+ const property = value[key];
166
+ return isObject(property) ? property : undefined;
167
+ }
168
+
169
+ function getStringProperty(value: Record<string, unknown>, key: string): string | undefined {
170
+ const property = value[key];
171
+ return typeof property === "string" && property.trim().length > 0 ? property : undefined;
172
+ }
173
+
174
+ function isObject(value: unknown): value is Record<string, unknown> {
175
+ return typeof value === "object" && value !== null && !Array.isArray(value);
176
+ }