@clinebot/llms 0.0.0

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 (219) hide show
  1. package/README.md +198 -0
  2. package/dist/config-browser.d.ts +3 -0
  3. package/dist/config.d.ts +3 -0
  4. package/dist/index.browser.d.ts +4 -0
  5. package/dist/index.browser.js +1 -0
  6. package/dist/index.d.ts +5 -0
  7. package/dist/index.js +7 -0
  8. package/dist/models/generated-access.d.ts +4 -0
  9. package/dist/models/generated-provider-loaders.d.ts +13 -0
  10. package/dist/models/generated.d.ts +14 -0
  11. package/dist/models/index.d.ts +43 -0
  12. package/dist/models/models-dev-catalog.d.ts +32 -0
  13. package/dist/models/providers/aihubmix.d.ts +5 -0
  14. package/dist/models/providers/anthropic.d.ts +53 -0
  15. package/dist/models/providers/asksage.d.ts +5 -0
  16. package/dist/models/providers/baseten.d.ts +5 -0
  17. package/dist/models/providers/bedrock.d.ts +7 -0
  18. package/dist/models/providers/cerebras.d.ts +7 -0
  19. package/dist/models/providers/claude-code.d.ts +4 -0
  20. package/dist/models/providers/cline.d.ts +34 -0
  21. package/dist/models/providers/deepseek.d.ts +8 -0
  22. package/dist/models/providers/dify.d.ts +5 -0
  23. package/dist/models/providers/doubao.d.ts +7 -0
  24. package/dist/models/providers/fireworks.d.ts +8 -0
  25. package/dist/models/providers/gemini.d.ts +9 -0
  26. package/dist/models/providers/groq.d.ts +8 -0
  27. package/dist/models/providers/hicap.d.ts +5 -0
  28. package/dist/models/providers/huawei-cloud-maas.d.ts +5 -0
  29. package/dist/models/providers/huggingface.d.ts +6 -0
  30. package/dist/models/providers/index.d.ts +45 -0
  31. package/dist/models/providers/litellm.d.ts +5 -0
  32. package/dist/models/providers/lmstudio.d.ts +5 -0
  33. package/dist/models/providers/minimax.d.ts +7 -0
  34. package/dist/models/providers/mistral.d.ts +5 -0
  35. package/dist/models/providers/moonshot.d.ts +7 -0
  36. package/dist/models/providers/nebius.d.ts +7 -0
  37. package/dist/models/providers/nous-research.d.ts +7 -0
  38. package/dist/models/providers/oca.d.ts +9 -0
  39. package/dist/models/providers/ollama.d.ts +5 -0
  40. package/dist/models/providers/openai-codex.d.ts +10 -0
  41. package/dist/models/providers/openai.d.ts +9 -0
  42. package/dist/models/providers/opencode.d.ts +10 -0
  43. package/dist/models/providers/openrouter.d.ts +7 -0
  44. package/dist/models/providers/qwen-code.d.ts +7 -0
  45. package/dist/models/providers/qwen.d.ts +7 -0
  46. package/dist/models/providers/requesty.d.ts +6 -0
  47. package/dist/models/providers/sambanova.d.ts +7 -0
  48. package/dist/models/providers/sapaicore.d.ts +7 -0
  49. package/dist/models/providers/together.d.ts +8 -0
  50. package/dist/models/providers/vercel-ai-gateway.d.ts +5 -0
  51. package/dist/models/providers/vertex.d.ts +7 -0
  52. package/dist/models/providers/xai.d.ts +8 -0
  53. package/dist/models/providers/zai.d.ts +7 -0
  54. package/dist/models/query.d.ts +181 -0
  55. package/dist/models/registry.d.ts +123 -0
  56. package/dist/models/schemas/index.d.ts +7 -0
  57. package/dist/models/schemas/model.d.ts +340 -0
  58. package/dist/models/schemas/query.d.ts +191 -0
  59. package/dist/providers/handlers/ai-sdk-community.d.ts +46 -0
  60. package/dist/providers/handlers/ai-sdk-provider-base.d.ts +32 -0
  61. package/dist/providers/handlers/anthropic-base.d.ts +26 -0
  62. package/dist/providers/handlers/asksage.d.ts +12 -0
  63. package/dist/providers/handlers/auth.d.ts +5 -0
  64. package/dist/providers/handlers/base.d.ts +55 -0
  65. package/dist/providers/handlers/bedrock-base.d.ts +23 -0
  66. package/dist/providers/handlers/bedrock-client.d.ts +4 -0
  67. package/dist/providers/handlers/community-sdk.d.ts +97 -0
  68. package/dist/providers/handlers/fetch-base.d.ts +18 -0
  69. package/dist/providers/handlers/gemini-base.d.ts +25 -0
  70. package/dist/providers/handlers/index.d.ts +19 -0
  71. package/dist/providers/handlers/openai-base.d.ts +54 -0
  72. package/dist/providers/handlers/openai-responses.d.ts +64 -0
  73. package/dist/providers/handlers/providers.d.ts +43 -0
  74. package/dist/providers/handlers/r1-base.d.ts +62 -0
  75. package/dist/providers/handlers/registry.d.ts +106 -0
  76. package/dist/providers/handlers/vertex.d.ts +32 -0
  77. package/dist/providers/index.d.ts +100 -0
  78. package/dist/providers/public.browser.d.ts +2 -0
  79. package/dist/providers/public.d.ts +3 -0
  80. package/dist/providers/shared/openai-compatible.d.ts +10 -0
  81. package/dist/providers/transform/ai-sdk-community-format.d.ts +9 -0
  82. package/dist/providers/transform/anthropic-format.d.ts +24 -0
  83. package/dist/providers/transform/content-format.d.ts +3 -0
  84. package/dist/providers/transform/gemini-format.d.ts +19 -0
  85. package/dist/providers/transform/index.d.ts +10 -0
  86. package/dist/providers/transform/openai-format.d.ts +36 -0
  87. package/dist/providers/transform/r1-format.d.ts +26 -0
  88. package/dist/providers/types/config.d.ts +261 -0
  89. package/dist/providers/types/handler.d.ts +71 -0
  90. package/dist/providers/types/index.d.ts +11 -0
  91. package/dist/providers/types/messages.d.ts +139 -0
  92. package/dist/providers/types/model-info.d.ts +32 -0
  93. package/dist/providers/types/provider-ids.d.ts +63 -0
  94. package/dist/providers/types/settings.d.ts +308 -0
  95. package/dist/providers/types/stream.d.ts +106 -0
  96. package/dist/providers/utils/index.d.ts +7 -0
  97. package/dist/providers/utils/retry.d.ts +38 -0
  98. package/dist/providers/utils/stream-processor.d.ts +110 -0
  99. package/dist/providers/utils/tool-processor.d.ts +34 -0
  100. package/dist/sdk.d.ts +18 -0
  101. package/dist/types.d.ts +60 -0
  102. package/package.json +66 -0
  103. package/src/catalog.ts +20 -0
  104. package/src/config-browser.ts +11 -0
  105. package/src/config.ts +49 -0
  106. package/src/index.browser.ts +9 -0
  107. package/src/index.ts +10 -0
  108. package/src/live-providers.test.ts +137 -0
  109. package/src/models/generated-access.ts +41 -0
  110. package/src/models/generated-provider-loaders.ts +166 -0
  111. package/src/models/generated.ts +11997 -0
  112. package/src/models/index.ts +271 -0
  113. package/src/models/models-dev-catalog.test.ts +161 -0
  114. package/src/models/models-dev-catalog.ts +161 -0
  115. package/src/models/providers/aihubmix.ts +19 -0
  116. package/src/models/providers/anthropic.ts +60 -0
  117. package/src/models/providers/asksage.ts +19 -0
  118. package/src/models/providers/baseten.ts +21 -0
  119. package/src/models/providers/bedrock.ts +30 -0
  120. package/src/models/providers/cerebras.ts +24 -0
  121. package/src/models/providers/claude-code.ts +51 -0
  122. package/src/models/providers/cline.ts +25 -0
  123. package/src/models/providers/deepseek.ts +33 -0
  124. package/src/models/providers/dify.ts +17 -0
  125. package/src/models/providers/doubao.ts +33 -0
  126. package/src/models/providers/fireworks.ts +34 -0
  127. package/src/models/providers/gemini.ts +43 -0
  128. package/src/models/providers/groq.ts +33 -0
  129. package/src/models/providers/hicap.ts +18 -0
  130. package/src/models/providers/huawei-cloud-maas.ts +18 -0
  131. package/src/models/providers/huggingface.ts +22 -0
  132. package/src/models/providers/index.ts +162 -0
  133. package/src/models/providers/litellm.ts +19 -0
  134. package/src/models/providers/lmstudio.ts +22 -0
  135. package/src/models/providers/minimax.ts +34 -0
  136. package/src/models/providers/mistral.ts +19 -0
  137. package/src/models/providers/moonshot.ts +34 -0
  138. package/src/models/providers/nebius.ts +24 -0
  139. package/src/models/providers/nous-research.ts +21 -0
  140. package/src/models/providers/oca.ts +30 -0
  141. package/src/models/providers/ollama.ts +18 -0
  142. package/src/models/providers/openai-codex.ts +30 -0
  143. package/src/models/providers/openai.ts +43 -0
  144. package/src/models/providers/opencode.ts +28 -0
  145. package/src/models/providers/openrouter.ts +24 -0
  146. package/src/models/providers/qwen-code.ts +33 -0
  147. package/src/models/providers/qwen.ts +34 -0
  148. package/src/models/providers/requesty.ts +23 -0
  149. package/src/models/providers/sambanova.ts +23 -0
  150. package/src/models/providers/sapaicore.ts +34 -0
  151. package/src/models/providers/together.ts +35 -0
  152. package/src/models/providers/vercel-ai-gateway.ts +23 -0
  153. package/src/models/providers/vertex.ts +36 -0
  154. package/src/models/providers/xai.ts +34 -0
  155. package/src/models/providers/zai.ts +25 -0
  156. package/src/models/query.ts +407 -0
  157. package/src/models/registry.ts +511 -0
  158. package/src/models/schemas/index.ts +62 -0
  159. package/src/models/schemas/model.ts +308 -0
  160. package/src/models/schemas/query.ts +336 -0
  161. package/src/providers/browser.ts +4 -0
  162. package/src/providers/handlers/ai-sdk-community.ts +226 -0
  163. package/src/providers/handlers/ai-sdk-provider-base.ts +193 -0
  164. package/src/providers/handlers/anthropic-base.ts +372 -0
  165. package/src/providers/handlers/asksage.test.ts +103 -0
  166. package/src/providers/handlers/asksage.ts +138 -0
  167. package/src/providers/handlers/auth.test.ts +19 -0
  168. package/src/providers/handlers/auth.ts +121 -0
  169. package/src/providers/handlers/base.test.ts +46 -0
  170. package/src/providers/handlers/base.ts +160 -0
  171. package/src/providers/handlers/bedrock-base.ts +390 -0
  172. package/src/providers/handlers/bedrock-client.ts +100 -0
  173. package/src/providers/handlers/codex.test.ts +123 -0
  174. package/src/providers/handlers/community-sdk.test.ts +288 -0
  175. package/src/providers/handlers/community-sdk.ts +392 -0
  176. package/src/providers/handlers/fetch-base.ts +68 -0
  177. package/src/providers/handlers/gemini-base.ts +302 -0
  178. package/src/providers/handlers/index.ts +67 -0
  179. package/src/providers/handlers/openai-base.ts +277 -0
  180. package/src/providers/handlers/openai-responses.ts +598 -0
  181. package/src/providers/handlers/providers.test.ts +120 -0
  182. package/src/providers/handlers/providers.ts +563 -0
  183. package/src/providers/handlers/r1-base.ts +280 -0
  184. package/src/providers/handlers/registry.ts +185 -0
  185. package/src/providers/handlers/vertex.test.ts +124 -0
  186. package/src/providers/handlers/vertex.ts +292 -0
  187. package/src/providers/index.ts +534 -0
  188. package/src/providers/public.browser.ts +20 -0
  189. package/src/providers/public.ts +51 -0
  190. package/src/providers/shared/openai-compatible.ts +63 -0
  191. package/src/providers/transform/ai-sdk-community-format.test.ts +73 -0
  192. package/src/providers/transform/ai-sdk-community-format.ts +115 -0
  193. package/src/providers/transform/anthropic-format.ts +218 -0
  194. package/src/providers/transform/content-format.ts +34 -0
  195. package/src/providers/transform/format-conversion.test.ts +310 -0
  196. package/src/providers/transform/gemini-format.ts +167 -0
  197. package/src/providers/transform/index.ts +22 -0
  198. package/src/providers/transform/openai-format.ts +247 -0
  199. package/src/providers/transform/r1-format.ts +287 -0
  200. package/src/providers/types/config.ts +388 -0
  201. package/src/providers/types/handler.ts +87 -0
  202. package/src/providers/types/index.ts +120 -0
  203. package/src/providers/types/messages.ts +158 -0
  204. package/src/providers/types/model-info.test.ts +57 -0
  205. package/src/providers/types/model-info.ts +65 -0
  206. package/src/providers/types/provider-ids.test.ts +12 -0
  207. package/src/providers/types/provider-ids.ts +89 -0
  208. package/src/providers/types/settings.test.ts +49 -0
  209. package/src/providers/types/settings.ts +533 -0
  210. package/src/providers/types/stream.ts +117 -0
  211. package/src/providers/utils/index.ts +27 -0
  212. package/src/providers/utils/retry.test.ts +140 -0
  213. package/src/providers/utils/retry.ts +188 -0
  214. package/src/providers/utils/stream-processor.test.ts +232 -0
  215. package/src/providers/utils/stream-processor.ts +472 -0
  216. package/src/providers/utils/tool-processor.test.ts +34 -0
  217. package/src/providers/utils/tool-processor.ts +111 -0
  218. package/src/sdk.ts +264 -0
  219. package/src/types.ts +79 -0
@@ -0,0 +1,598 @@
1
+ /**
2
+ * OpenAI Responses API Handler
3
+ *
4
+ * Handler for OpenAI's Responses API format, which is used by newer models
5
+ * that require native tool calling (e.g., GPT-5, o3, codex).
6
+ *
7
+ * The Responses API has a different structure than Chat Completions:
8
+ * - Uses `instructions` instead of system messages
9
+ * - Uses `input` instead of messages array
10
+ * - Has different streaming event types (response.*, not choices.delta)
11
+ * - Supports reasoning with encrypted content
12
+ */
13
+
14
+ import OpenAI from "openai";
15
+ import type {
16
+ ApiStream,
17
+ HandlerModelInfo,
18
+ ModelInfo,
19
+ ProviderConfig,
20
+ } from "../types";
21
+ import type { Message, ToolDefinition } from "../types/messages";
22
+ import { retryStream } from "../utils/retry";
23
+ import { getMissingApiKeyError, resolveApiKeyForProvider } from "./auth";
24
+ import { BaseHandler } from "./base";
25
+
26
+ const DEFAULT_REASONING_EFFORT = "medium" as const;
27
+
28
+ function normalizeStrictToolSchema(
29
+ schema: unknown,
30
+ options?: { stripFormat?: boolean },
31
+ ): unknown {
32
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
33
+ return schema;
34
+ }
35
+
36
+ const normalized = { ...(schema as Record<string, unknown>) };
37
+ if (options?.stripFormat && "format" in normalized) {
38
+ delete normalized.format;
39
+ }
40
+ const type = normalized.type;
41
+
42
+ if (type === "object") {
43
+ if (!Object.hasOwn(normalized, "additionalProperties")) {
44
+ normalized.additionalProperties = false;
45
+ }
46
+ const properties = normalized.properties;
47
+ if (
48
+ properties &&
49
+ typeof properties === "object" &&
50
+ !Array.isArray(properties)
51
+ ) {
52
+ const nextProperties: Record<string, unknown> = {};
53
+ for (const [key, value] of Object.entries(
54
+ properties as Record<string, unknown>,
55
+ )) {
56
+ nextProperties[key] = normalizeStrictToolSchema(value, options);
57
+ }
58
+ normalized.properties = nextProperties;
59
+ }
60
+ }
61
+
62
+ if (Array.isArray(normalized.anyOf)) {
63
+ normalized.anyOf = normalized.anyOf.map((item) =>
64
+ normalizeStrictToolSchema(item, options),
65
+ );
66
+ }
67
+ if (Array.isArray(normalized.oneOf)) {
68
+ normalized.oneOf = normalized.oneOf.map((item) =>
69
+ normalizeStrictToolSchema(item, options),
70
+ );
71
+ }
72
+ if (Array.isArray(normalized.allOf)) {
73
+ normalized.allOf = normalized.allOf.map((item) =>
74
+ normalizeStrictToolSchema(item, options),
75
+ );
76
+ }
77
+ if (normalized.not && typeof normalized.not === "object") {
78
+ normalized.not = normalizeStrictToolSchema(normalized.not, options);
79
+ }
80
+ if (normalized.items) {
81
+ if (Array.isArray(normalized.items)) {
82
+ normalized.items = normalized.items.map((item) =>
83
+ normalizeStrictToolSchema(item, options),
84
+ );
85
+ } else {
86
+ normalized.items = normalizeStrictToolSchema(normalized.items, options);
87
+ }
88
+ }
89
+
90
+ return normalized;
91
+ }
92
+
93
+ /**
94
+ * Convert tool definitions to Responses API format
95
+ */
96
+ function convertToolsToResponsesFormat(
97
+ tools?: ToolDefinition[],
98
+ options?: { stripFormat?: boolean },
99
+ ) {
100
+ if (!tools?.length) return undefined;
101
+
102
+ return tools.map((tool) => ({
103
+ type: "function" as const,
104
+ name: tool.name,
105
+ description: tool.description,
106
+ parameters: normalizeStrictToolSchema(tool.inputSchema, options),
107
+ strict: true, // Responses API defaults to strict mode
108
+ }));
109
+ }
110
+
111
+ /**
112
+ * Convert messages to Responses API input format
113
+ */
114
+ function convertToResponsesInput(messages: Message[]) {
115
+ // Responses API uses a flat input array with specific item types
116
+ const input: Array<{
117
+ type: "message";
118
+ role: "user" | "assistant";
119
+ content:
120
+ | string
121
+ | Array<{ type: "input_text" | "output_text"; text: string }>;
122
+ }> = [];
123
+
124
+ for (const msg of messages) {
125
+ if (msg.role === "user" || msg.role === "assistant") {
126
+ // Handle content blocks
127
+ const contentBlocks = Array.isArray(msg.content)
128
+ ? msg.content
129
+ : [{ type: "text" as const, text: msg.content }];
130
+
131
+ const textContent = contentBlocks
132
+ .filter(
133
+ (block): block is { type: "text"; text: string } =>
134
+ block.type === "text",
135
+ )
136
+ .map((block) => block.text)
137
+ .join("\n");
138
+
139
+ if (textContent) {
140
+ input.push({
141
+ type: "message",
142
+ role: msg.role,
143
+ content:
144
+ msg.role === "user"
145
+ ? [{ type: "input_text", text: textContent }]
146
+ : [{ type: "output_text", text: textContent }],
147
+ });
148
+ }
149
+ }
150
+ }
151
+
152
+ return input;
153
+ }
154
+
155
+ /**
156
+ * Handler for OpenAI Responses API
157
+ *
158
+ * Uses ProviderConfig fields:
159
+ * - baseUrl: Base URL for the API
160
+ * - modelId: Model ID
161
+ * - knownModels: Known models with their info
162
+ * - headers: Custom headers
163
+ */
164
+ export class OpenAIResponsesHandler extends BaseHandler {
165
+ protected client: OpenAI | undefined;
166
+
167
+ /**
168
+ * Ensure the OpenAI client is initialized
169
+ */
170
+ protected ensureClient(): OpenAI {
171
+ if (!this.client) {
172
+ const baseURL = this.config.baseUrl;
173
+
174
+ if (!baseURL) {
175
+ throw new Error("Base URL is required. Set baseUrl in config.");
176
+ }
177
+ const apiKey = resolveApiKeyForProvider(
178
+ this.config.providerId,
179
+ this.config.apiKey,
180
+ );
181
+ if (!apiKey) {
182
+ throw new Error(getMissingApiKeyError(this.config.providerId));
183
+ }
184
+ const requestHeaders = this.getRequestHeaders();
185
+ const hasAuthorizationHeader = Object.keys(requestHeaders).some(
186
+ (key) => key.toLowerCase() === "authorization",
187
+ );
188
+
189
+ this.client = new OpenAI({
190
+ apiKey,
191
+ baseURL,
192
+ defaultHeaders: hasAuthorizationHeader
193
+ ? requestHeaders
194
+ : { ...requestHeaders, Authorization: `Bearer ${apiKey}` },
195
+ });
196
+ }
197
+ return this.client;
198
+ }
199
+
200
+ /**
201
+ * Get model info, falling back to provider defaults
202
+ */
203
+ getModel(): HandlerModelInfo {
204
+ const modelId = this.config.modelId;
205
+ if (!modelId) {
206
+ throw new Error("Model ID is required. Set modelId in config.");
207
+ }
208
+
209
+ const modelInfo =
210
+ this.config.modelInfo ??
211
+ this.config.knownModels?.[modelId] ??
212
+ this.getDefaultModelInfo();
213
+
214
+ return { id: modelId, info: { ...modelInfo, id: modelId } };
215
+ }
216
+
217
+ protected getDefaultModelInfo(): ModelInfo {
218
+ // Responses API models don't support prompt caching
219
+ const capabilities = [].filter((c) => c !== "prompt-cache");
220
+ return {
221
+ id: this.config.modelId,
222
+ capabilities,
223
+ };
224
+ }
225
+
226
+ getMessages(
227
+ _systemPrompt: string,
228
+ messages: Message[],
229
+ ): ReturnType<typeof convertToResponsesInput> {
230
+ return convertToResponsesInput(messages);
231
+ }
232
+
233
+ /**
234
+ * Create a streaming message using the Responses API
235
+ */
236
+ async *createMessage(
237
+ systemPrompt: string,
238
+ messages: Message[],
239
+ tools?: ToolDefinition[],
240
+ ): ApiStream {
241
+ yield* retryStream(() =>
242
+ this.createMessageInternal(systemPrompt, messages, tools),
243
+ );
244
+ }
245
+
246
+ private async *createMessageInternal(
247
+ systemPrompt: string,
248
+ messages: Message[],
249
+ tools?: ToolDefinition[],
250
+ ): ApiStream {
251
+ const client = this.ensureClient();
252
+ const { id: modelId, info: modelInfo } = this.getModel();
253
+ const abortSignal = this.getAbortSignal();
254
+ const fallbackResponseId = this.createResponseId();
255
+ let resolvedResponseId: string | undefined;
256
+
257
+ // Convert messages to Responses API input format
258
+ const input = this.getMessages(systemPrompt, messages);
259
+
260
+ // Convert tools to Responses API format
261
+ const responseTools = convertToolsToResponsesFormat(tools, {
262
+ stripFormat: this.config.providerId === "openai-codex",
263
+ });
264
+
265
+ // Responses API requires tools for native tool calling
266
+ if (!responseTools?.length) {
267
+ throw new Error(
268
+ "OpenAI Responses API requires tools to be provided. Enable native tool calling in settings.",
269
+ );
270
+ }
271
+
272
+ // Build reasoning config
273
+ const supportsReasoning =
274
+ modelInfo.capabilities?.includes("reasoning") ?? false;
275
+ const effectiveReasoningEffort =
276
+ this.config.reasoningEffort ??
277
+ (this.config.thinking ? DEFAULT_REASONING_EFFORT : undefined);
278
+ const reasoningConfig =
279
+ supportsReasoning && effectiveReasoningEffort
280
+ ? {
281
+ effort: effectiveReasoningEffort,
282
+ summary: "auto" as const,
283
+ }
284
+ : undefined;
285
+ const requestHeaders = this.getRequestHeaders();
286
+ const hasAuthorizationHeader = Object.keys(requestHeaders).some(
287
+ (key) => key.toLowerCase() === "authorization",
288
+ );
289
+ const apiKey = resolveApiKeyForProvider(
290
+ this.config.providerId,
291
+ this.config.apiKey,
292
+ );
293
+ if (!hasAuthorizationHeader && apiKey) {
294
+ requestHeaders.Authorization = `Bearer ${apiKey}`;
295
+ }
296
+ if (
297
+ this.config.providerId === "openai-codex" &&
298
+ typeof this.config.accountId === "string" &&
299
+ this.config.accountId.trim().length > 0
300
+ ) {
301
+ const accountId = this.config.accountId.trim();
302
+ // ChatGPT Codex endpoints may require an explicit account identifier.
303
+ requestHeaders["chatgpt-account-id"] = accountId;
304
+ requestHeaders["openai-account-id"] = accountId;
305
+ }
306
+
307
+ // Create the response using Responses API
308
+ let stream: AsyncIterable<any>;
309
+ try {
310
+ stream = await (client as any).responses.create(
311
+ {
312
+ model: modelId,
313
+ instructions: systemPrompt,
314
+ input,
315
+ // ChatGPT account Codex rejects requests unless explicit non-storage is set.
316
+ store: this.config.providerId === "openai-codex" ? false : undefined,
317
+ stream: true,
318
+ tools: responseTools,
319
+ reasoning: reasoningConfig,
320
+ },
321
+ { signal: abortSignal, headers: requestHeaders },
322
+ );
323
+ } catch (error) {
324
+ if (this.config.providerId === "openai-codex") {
325
+ const rawError = error as
326
+ | (Error & {
327
+ status?: number;
328
+ message?: string;
329
+ error?: { message?: string; detail?: string };
330
+ response?: { status?: number };
331
+ })
332
+ | undefined;
333
+ const status =
334
+ rawError?.status ??
335
+ rawError?.response?.status ??
336
+ (typeof rawError?.message === "string" &&
337
+ rawError.message.includes("400")
338
+ ? 400
339
+ : undefined);
340
+ if (status === 400) {
341
+ const detail =
342
+ rawError?.error?.detail ??
343
+ rawError?.error?.message ??
344
+ (typeof rawError?.message === "string" ? rawError.message : "");
345
+ throw new Error(
346
+ `OpenAI Codex request was rejected (HTTP 400). ${detail ? `Detail: ${detail}` : "Re-run 'clite auth openai-codex', verify model access, and ensure accountId is present in provider settings."}`,
347
+ { cause: error },
348
+ );
349
+ }
350
+ }
351
+ throw error;
352
+ }
353
+
354
+ // Process the response stream
355
+ for await (const chunk of stream) {
356
+ const apiResponseId = this.getApiResponseId(chunk);
357
+ if (apiResponseId) {
358
+ resolvedResponseId = apiResponseId;
359
+ }
360
+
361
+ yield* this.processResponseChunk(
362
+ chunk,
363
+ modelInfo,
364
+ resolvedResponseId ?? fallbackResponseId,
365
+ );
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Process a single chunk from the Responses API stream
371
+ */
372
+ protected *processResponseChunk(
373
+ chunk: any,
374
+ _modelInfo: ModelInfo,
375
+ responseId: string,
376
+ ): Generator<import("../types").ApiStreamChunk> {
377
+ // Handle different event types from Responses API
378
+ switch (chunk.type) {
379
+ case "response.output_item.added": {
380
+ const item = chunk.item;
381
+ if (item.type === "function_call" && item.id) {
382
+ yield {
383
+ type: "tool_calls",
384
+ id: item.id || responseId,
385
+ tool_call: {
386
+ call_id: item.call_id,
387
+ function: {
388
+ id: item.id,
389
+ name: item.name,
390
+ arguments: item.arguments,
391
+ },
392
+ },
393
+ };
394
+ }
395
+ if (item.type === "reasoning" && item.encrypted_content && item.id) {
396
+ yield {
397
+ type: "reasoning",
398
+ id: item.id || responseId,
399
+ reasoning: "",
400
+ redacted_data: item.encrypted_content,
401
+ };
402
+ }
403
+ break;
404
+ }
405
+
406
+ case "response.output_item.done": {
407
+ const item = chunk.item;
408
+ if (item.type === "function_call") {
409
+ yield {
410
+ type: "tool_calls",
411
+ id: item.id || responseId,
412
+ tool_call: {
413
+ call_id: item.call_id,
414
+ function: {
415
+ id: item.id,
416
+ name: item.name,
417
+ arguments: item.arguments,
418
+ },
419
+ },
420
+ };
421
+ }
422
+ if (item.type === "reasoning") {
423
+ yield {
424
+ type: "reasoning",
425
+ id: item.id || responseId,
426
+ details: item.summary,
427
+ reasoning: "",
428
+ };
429
+ }
430
+ break;
431
+ }
432
+
433
+ case "response.reasoning_summary_part.added":
434
+ yield {
435
+ type: "reasoning",
436
+ id: chunk.item_id || responseId,
437
+ reasoning: chunk.part?.text || "",
438
+ };
439
+ break;
440
+
441
+ case "response.reasoning_summary_text.delta":
442
+ yield {
443
+ type: "reasoning",
444
+ id: chunk.item_id || responseId,
445
+ reasoning: chunk.delta || "",
446
+ };
447
+ break;
448
+
449
+ case "response.reasoning_summary_part.done":
450
+ yield {
451
+ type: "reasoning",
452
+ id: chunk.item_id || responseId,
453
+ details: chunk.part,
454
+ reasoning: "",
455
+ };
456
+ break;
457
+
458
+ case "response.output_text.delta":
459
+ if (chunk.delta) {
460
+ yield {
461
+ id: chunk.item_id || responseId,
462
+ type: "text",
463
+ text: chunk.delta,
464
+ };
465
+ }
466
+ break;
467
+
468
+ case "response.reasoning_text.delta":
469
+ if (chunk.delta) {
470
+ yield {
471
+ id: chunk.item_id || responseId,
472
+ type: "reasoning",
473
+ reasoning: chunk.delta,
474
+ };
475
+ }
476
+ break;
477
+
478
+ case "response.function_call_arguments.delta":
479
+ yield {
480
+ type: "tool_calls",
481
+ id: chunk.item_id || responseId,
482
+ tool_call: {
483
+ function: {
484
+ id: chunk.item_id,
485
+ name: chunk.item_id,
486
+ arguments: chunk.delta,
487
+ },
488
+ },
489
+ };
490
+ break;
491
+
492
+ case "response.function_call_arguments.done":
493
+ if (chunk.item_id && chunk.name && chunk.arguments) {
494
+ yield {
495
+ type: "tool_calls",
496
+ id: chunk.item_id || responseId,
497
+ tool_call: {
498
+ function: {
499
+ id: chunk.item_id,
500
+ name: chunk.name,
501
+ arguments: chunk.arguments,
502
+ },
503
+ },
504
+ };
505
+ }
506
+ break;
507
+
508
+ case "response.incomplete": {
509
+ const incompleteReason = chunk.response?.incomplete_details?.reason;
510
+ yield {
511
+ type: "done",
512
+ success: false,
513
+ incompleteReason,
514
+ id: chunk.response?.id || responseId,
515
+ };
516
+ break;
517
+ }
518
+
519
+ case "response.failed": {
520
+ const error = chunk.response?.error;
521
+ yield {
522
+ type: "done",
523
+ success: false,
524
+ error: error?.message || "Unknown error",
525
+ id: chunk.response?.id || responseId,
526
+ };
527
+ break;
528
+ }
529
+
530
+ case "response.completed": {
531
+ if (chunk.response?.usage) {
532
+ const usage = chunk.response.usage;
533
+ const inputTokens = usage.input_tokens || 0;
534
+ const outputTokens = usage.output_tokens || 0;
535
+ const cacheReadTokens =
536
+ usage.output_tokens_details?.reasoning_tokens || 0;
537
+ const cacheWriteTokens =
538
+ usage.input_tokens_details?.cached_tokens || 0;
539
+
540
+ const totalCost = this.calculateCost(
541
+ inputTokens,
542
+ outputTokens,
543
+ cacheReadTokens,
544
+ );
545
+ const nonCachedInputTokens = Math.max(
546
+ 0,
547
+ inputTokens - cacheReadTokens - cacheWriteTokens,
548
+ );
549
+
550
+ yield {
551
+ type: "usage",
552
+ inputTokens: nonCachedInputTokens,
553
+ outputTokens,
554
+ cacheWriteTokens,
555
+ cacheReadTokens,
556
+ totalCost,
557
+ id: chunk.response.id || responseId,
558
+ };
559
+ }
560
+
561
+ // Yield done chunk to indicate streaming completed successfully
562
+ yield {
563
+ type: "done",
564
+ success: true,
565
+ id: chunk.response?.id || responseId,
566
+ };
567
+ break;
568
+ }
569
+ }
570
+ }
571
+
572
+ private getApiResponseId(chunk: any): string | undefined {
573
+ if (
574
+ typeof chunk?.response?.id === "string" &&
575
+ chunk.response.id.length > 0
576
+ ) {
577
+ return chunk.response.id;
578
+ }
579
+
580
+ if (
581
+ typeof chunk?.response_id === "string" &&
582
+ chunk.response_id.length > 0
583
+ ) {
584
+ return chunk.response_id;
585
+ }
586
+
587
+ return undefined;
588
+ }
589
+ }
590
+
591
+ /**
592
+ * Create an OpenAI Responses API handler
593
+ */
594
+ export function createOpenAIResponsesHandler(
595
+ config: ProviderConfig,
596
+ ): OpenAIResponsesHandler {
597
+ return new OpenAIResponsesHandler(config);
598
+ }
@@ -0,0 +1,120 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import type { ModelInfo, ProviderConfig } from "../types/index";
3
+ import {
4
+ clearLiveModelsCatalogCache,
5
+ clearPrivateModelsCatalogCache,
6
+ OPENAI_COMPATIBLE_PROVIDERS,
7
+ resolveProviderConfig,
8
+ } from "./providers";
9
+
10
+ function createLiteLlmConfig(
11
+ overrides?: Partial<ProviderConfig>,
12
+ ): ProviderConfig {
13
+ return {
14
+ providerId: "litellm",
15
+ modelId: "gpt-4o",
16
+ apiKey: "test-key",
17
+ baseUrl: "http://localhost:4000",
18
+ ...overrides,
19
+ };
20
+ }
21
+
22
+ describe("resolveProviderConfig", () => {
23
+ const originalFetch = globalThis.fetch;
24
+
25
+ beforeEach(() => {
26
+ clearLiveModelsCatalogCache();
27
+ clearPrivateModelsCatalogCache();
28
+ });
29
+
30
+ afterEach(() => {
31
+ globalThis.fetch = originalFetch;
32
+ vi.restoreAllMocks();
33
+ });
34
+
35
+ it("loads auth-gated private models and gives user knownModels highest priority", async () => {
36
+ const fetchMock = vi.fn(async () => ({
37
+ ok: true,
38
+ json: async () => ({
39
+ data: [
40
+ {
41
+ model_name: "gpt-4o",
42
+ litellm_params: { model: "gpt-4o" },
43
+ model_info: {
44
+ max_tokens: 32000,
45
+ max_input_tokens: 128000,
46
+ },
47
+ },
48
+ ],
49
+ }),
50
+ }));
51
+ globalThis.fetch = fetchMock as unknown as typeof fetch;
52
+
53
+ const userModel: ModelInfo = {
54
+ id: "gpt-4o",
55
+ name: "User Override",
56
+ contextWindow: 999_999,
57
+ maxTokens: 9_999,
58
+ capabilities: ["streaming", "tools"],
59
+ };
60
+
61
+ const resolved = await resolveProviderConfig(
62
+ "litellm",
63
+ { loadPrivateOnAuth: true },
64
+ createLiteLlmConfig({
65
+ knownModels: {
66
+ "gpt-4o": userModel,
67
+ },
68
+ }),
69
+ );
70
+
71
+ expect(fetchMock).toHaveBeenCalledTimes(1);
72
+ expect(resolved?.knownModels?.["gpt-4o"]).toEqual(userModel);
73
+ });
74
+
75
+ it("caches auth-gated private model responses by provider+baseUrl+token", async () => {
76
+ const fetchMock = vi.fn(async () => ({
77
+ ok: true,
78
+ json: async () => ({
79
+ data: [
80
+ {
81
+ model_name: "proxy-model",
82
+ litellm_params: { model: "proxy-model" },
83
+ model_info: {},
84
+ },
85
+ ],
86
+ }),
87
+ }));
88
+ globalThis.fetch = fetchMock as unknown as typeof fetch;
89
+
90
+ const config = createLiteLlmConfig();
91
+ await resolveProviderConfig("litellm", { loadPrivateOnAuth: true }, config);
92
+ await resolveProviderConfig("litellm", { loadPrivateOnAuth: true }, config);
93
+
94
+ expect(fetchMock).toHaveBeenCalledTimes(1);
95
+ });
96
+
97
+ it("does not load auth-gated private models when disabled", async () => {
98
+ const fetchMock = vi.fn();
99
+ globalThis.fetch = fetchMock as unknown as typeof fetch;
100
+
101
+ await resolveProviderConfig(
102
+ "litellm",
103
+ { loadPrivateOnAuth: false },
104
+ createLiteLlmConfig(),
105
+ );
106
+
107
+ expect(fetchMock).not.toHaveBeenCalled();
108
+ });
109
+
110
+ it("includes lmstudio and zai as OpenAI-compatible providers", () => {
111
+ expect(OPENAI_COMPATIBLE_PROVIDERS).toHaveProperty("lmstudio");
112
+ expect(OPENAI_COMPATIBLE_PROVIDERS).toHaveProperty("zai");
113
+ expect(OPENAI_COMPATIBLE_PROVIDERS.lmstudio?.baseUrl).toBe(
114
+ "http://localhost:1234/v1",
115
+ );
116
+ expect(OPENAI_COMPATIBLE_PROVIDERS.zai?.baseUrl).toBe(
117
+ "https://api.z.ai/api/paas/v4",
118
+ );
119
+ });
120
+ });