@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.
- package/README.md +198 -0
- package/dist/config-browser.d.ts +3 -0
- package/dist/config.d.ts +3 -0
- package/dist/index.browser.d.ts +4 -0
- package/dist/index.browser.js +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +7 -0
- package/dist/models/generated-access.d.ts +4 -0
- package/dist/models/generated-provider-loaders.d.ts +13 -0
- package/dist/models/generated.d.ts +14 -0
- package/dist/models/index.d.ts +43 -0
- package/dist/models/models-dev-catalog.d.ts +32 -0
- package/dist/models/providers/aihubmix.d.ts +5 -0
- package/dist/models/providers/anthropic.d.ts +53 -0
- package/dist/models/providers/asksage.d.ts +5 -0
- package/dist/models/providers/baseten.d.ts +5 -0
- package/dist/models/providers/bedrock.d.ts +7 -0
- package/dist/models/providers/cerebras.d.ts +7 -0
- package/dist/models/providers/claude-code.d.ts +4 -0
- package/dist/models/providers/cline.d.ts +34 -0
- package/dist/models/providers/deepseek.d.ts +8 -0
- package/dist/models/providers/dify.d.ts +5 -0
- package/dist/models/providers/doubao.d.ts +7 -0
- package/dist/models/providers/fireworks.d.ts +8 -0
- package/dist/models/providers/gemini.d.ts +9 -0
- package/dist/models/providers/groq.d.ts +8 -0
- package/dist/models/providers/hicap.d.ts +5 -0
- package/dist/models/providers/huawei-cloud-maas.d.ts +5 -0
- package/dist/models/providers/huggingface.d.ts +6 -0
- package/dist/models/providers/index.d.ts +45 -0
- package/dist/models/providers/litellm.d.ts +5 -0
- package/dist/models/providers/lmstudio.d.ts +5 -0
- package/dist/models/providers/minimax.d.ts +7 -0
- package/dist/models/providers/mistral.d.ts +5 -0
- package/dist/models/providers/moonshot.d.ts +7 -0
- package/dist/models/providers/nebius.d.ts +7 -0
- package/dist/models/providers/nous-research.d.ts +7 -0
- package/dist/models/providers/oca.d.ts +9 -0
- package/dist/models/providers/ollama.d.ts +5 -0
- package/dist/models/providers/openai-codex.d.ts +10 -0
- package/dist/models/providers/openai.d.ts +9 -0
- package/dist/models/providers/opencode.d.ts +10 -0
- package/dist/models/providers/openrouter.d.ts +7 -0
- package/dist/models/providers/qwen-code.d.ts +7 -0
- package/dist/models/providers/qwen.d.ts +7 -0
- package/dist/models/providers/requesty.d.ts +6 -0
- package/dist/models/providers/sambanova.d.ts +7 -0
- package/dist/models/providers/sapaicore.d.ts +7 -0
- package/dist/models/providers/together.d.ts +8 -0
- package/dist/models/providers/vercel-ai-gateway.d.ts +5 -0
- package/dist/models/providers/vertex.d.ts +7 -0
- package/dist/models/providers/xai.d.ts +8 -0
- package/dist/models/providers/zai.d.ts +7 -0
- package/dist/models/query.d.ts +181 -0
- package/dist/models/registry.d.ts +123 -0
- package/dist/models/schemas/index.d.ts +7 -0
- package/dist/models/schemas/model.d.ts +340 -0
- package/dist/models/schemas/query.d.ts +191 -0
- package/dist/providers/handlers/ai-sdk-community.d.ts +46 -0
- package/dist/providers/handlers/ai-sdk-provider-base.d.ts +32 -0
- package/dist/providers/handlers/anthropic-base.d.ts +26 -0
- package/dist/providers/handlers/asksage.d.ts +12 -0
- package/dist/providers/handlers/auth.d.ts +5 -0
- package/dist/providers/handlers/base.d.ts +55 -0
- package/dist/providers/handlers/bedrock-base.d.ts +23 -0
- package/dist/providers/handlers/bedrock-client.d.ts +4 -0
- package/dist/providers/handlers/community-sdk.d.ts +97 -0
- package/dist/providers/handlers/fetch-base.d.ts +18 -0
- package/dist/providers/handlers/gemini-base.d.ts +25 -0
- package/dist/providers/handlers/index.d.ts +19 -0
- package/dist/providers/handlers/openai-base.d.ts +54 -0
- package/dist/providers/handlers/openai-responses.d.ts +64 -0
- package/dist/providers/handlers/providers.d.ts +43 -0
- package/dist/providers/handlers/r1-base.d.ts +62 -0
- package/dist/providers/handlers/registry.d.ts +106 -0
- package/dist/providers/handlers/vertex.d.ts +32 -0
- package/dist/providers/index.d.ts +100 -0
- package/dist/providers/public.browser.d.ts +2 -0
- package/dist/providers/public.d.ts +3 -0
- package/dist/providers/shared/openai-compatible.d.ts +10 -0
- package/dist/providers/transform/ai-sdk-community-format.d.ts +9 -0
- package/dist/providers/transform/anthropic-format.d.ts +24 -0
- package/dist/providers/transform/content-format.d.ts +3 -0
- package/dist/providers/transform/gemini-format.d.ts +19 -0
- package/dist/providers/transform/index.d.ts +10 -0
- package/dist/providers/transform/openai-format.d.ts +36 -0
- package/dist/providers/transform/r1-format.d.ts +26 -0
- package/dist/providers/types/config.d.ts +261 -0
- package/dist/providers/types/handler.d.ts +71 -0
- package/dist/providers/types/index.d.ts +11 -0
- package/dist/providers/types/messages.d.ts +139 -0
- package/dist/providers/types/model-info.d.ts +32 -0
- package/dist/providers/types/provider-ids.d.ts +63 -0
- package/dist/providers/types/settings.d.ts +308 -0
- package/dist/providers/types/stream.d.ts +106 -0
- package/dist/providers/utils/index.d.ts +7 -0
- package/dist/providers/utils/retry.d.ts +38 -0
- package/dist/providers/utils/stream-processor.d.ts +110 -0
- package/dist/providers/utils/tool-processor.d.ts +34 -0
- package/dist/sdk.d.ts +18 -0
- package/dist/types.d.ts +60 -0
- package/package.json +66 -0
- package/src/catalog.ts +20 -0
- package/src/config-browser.ts +11 -0
- package/src/config.ts +49 -0
- package/src/index.browser.ts +9 -0
- package/src/index.ts +10 -0
- package/src/live-providers.test.ts +137 -0
- package/src/models/generated-access.ts +41 -0
- package/src/models/generated-provider-loaders.ts +166 -0
- package/src/models/generated.ts +11997 -0
- package/src/models/index.ts +271 -0
- package/src/models/models-dev-catalog.test.ts +161 -0
- package/src/models/models-dev-catalog.ts +161 -0
- package/src/models/providers/aihubmix.ts +19 -0
- package/src/models/providers/anthropic.ts +60 -0
- package/src/models/providers/asksage.ts +19 -0
- package/src/models/providers/baseten.ts +21 -0
- package/src/models/providers/bedrock.ts +30 -0
- package/src/models/providers/cerebras.ts +24 -0
- package/src/models/providers/claude-code.ts +51 -0
- package/src/models/providers/cline.ts +25 -0
- package/src/models/providers/deepseek.ts +33 -0
- package/src/models/providers/dify.ts +17 -0
- package/src/models/providers/doubao.ts +33 -0
- package/src/models/providers/fireworks.ts +34 -0
- package/src/models/providers/gemini.ts +43 -0
- package/src/models/providers/groq.ts +33 -0
- package/src/models/providers/hicap.ts +18 -0
- package/src/models/providers/huawei-cloud-maas.ts +18 -0
- package/src/models/providers/huggingface.ts +22 -0
- package/src/models/providers/index.ts +162 -0
- package/src/models/providers/litellm.ts +19 -0
- package/src/models/providers/lmstudio.ts +22 -0
- package/src/models/providers/minimax.ts +34 -0
- package/src/models/providers/mistral.ts +19 -0
- package/src/models/providers/moonshot.ts +34 -0
- package/src/models/providers/nebius.ts +24 -0
- package/src/models/providers/nous-research.ts +21 -0
- package/src/models/providers/oca.ts +30 -0
- package/src/models/providers/ollama.ts +18 -0
- package/src/models/providers/openai-codex.ts +30 -0
- package/src/models/providers/openai.ts +43 -0
- package/src/models/providers/opencode.ts +28 -0
- package/src/models/providers/openrouter.ts +24 -0
- package/src/models/providers/qwen-code.ts +33 -0
- package/src/models/providers/qwen.ts +34 -0
- package/src/models/providers/requesty.ts +23 -0
- package/src/models/providers/sambanova.ts +23 -0
- package/src/models/providers/sapaicore.ts +34 -0
- package/src/models/providers/together.ts +35 -0
- package/src/models/providers/vercel-ai-gateway.ts +23 -0
- package/src/models/providers/vertex.ts +36 -0
- package/src/models/providers/xai.ts +34 -0
- package/src/models/providers/zai.ts +25 -0
- package/src/models/query.ts +407 -0
- package/src/models/registry.ts +511 -0
- package/src/models/schemas/index.ts +62 -0
- package/src/models/schemas/model.ts +308 -0
- package/src/models/schemas/query.ts +336 -0
- package/src/providers/browser.ts +4 -0
- package/src/providers/handlers/ai-sdk-community.ts +226 -0
- package/src/providers/handlers/ai-sdk-provider-base.ts +193 -0
- package/src/providers/handlers/anthropic-base.ts +372 -0
- package/src/providers/handlers/asksage.test.ts +103 -0
- package/src/providers/handlers/asksage.ts +138 -0
- package/src/providers/handlers/auth.test.ts +19 -0
- package/src/providers/handlers/auth.ts +121 -0
- package/src/providers/handlers/base.test.ts +46 -0
- package/src/providers/handlers/base.ts +160 -0
- package/src/providers/handlers/bedrock-base.ts +390 -0
- package/src/providers/handlers/bedrock-client.ts +100 -0
- package/src/providers/handlers/codex.test.ts +123 -0
- package/src/providers/handlers/community-sdk.test.ts +288 -0
- package/src/providers/handlers/community-sdk.ts +392 -0
- package/src/providers/handlers/fetch-base.ts +68 -0
- package/src/providers/handlers/gemini-base.ts +302 -0
- package/src/providers/handlers/index.ts +67 -0
- package/src/providers/handlers/openai-base.ts +277 -0
- package/src/providers/handlers/openai-responses.ts +598 -0
- package/src/providers/handlers/providers.test.ts +120 -0
- package/src/providers/handlers/providers.ts +563 -0
- package/src/providers/handlers/r1-base.ts +280 -0
- package/src/providers/handlers/registry.ts +185 -0
- package/src/providers/handlers/vertex.test.ts +124 -0
- package/src/providers/handlers/vertex.ts +292 -0
- package/src/providers/index.ts +534 -0
- package/src/providers/public.browser.ts +20 -0
- package/src/providers/public.ts +51 -0
- package/src/providers/shared/openai-compatible.ts +63 -0
- package/src/providers/transform/ai-sdk-community-format.test.ts +73 -0
- package/src/providers/transform/ai-sdk-community-format.ts +115 -0
- package/src/providers/transform/anthropic-format.ts +218 -0
- package/src/providers/transform/content-format.ts +34 -0
- package/src/providers/transform/format-conversion.test.ts +310 -0
- package/src/providers/transform/gemini-format.ts +167 -0
- package/src/providers/transform/index.ts +22 -0
- package/src/providers/transform/openai-format.ts +247 -0
- package/src/providers/transform/r1-format.ts +287 -0
- package/src/providers/types/config.ts +388 -0
- package/src/providers/types/handler.ts +87 -0
- package/src/providers/types/index.ts +120 -0
- package/src/providers/types/messages.ts +158 -0
- package/src/providers/types/model-info.test.ts +57 -0
- package/src/providers/types/model-info.ts +65 -0
- package/src/providers/types/provider-ids.test.ts +12 -0
- package/src/providers/types/provider-ids.ts +89 -0
- package/src/providers/types/settings.test.ts +49 -0
- package/src/providers/types/settings.ts +533 -0
- package/src/providers/types/stream.ts +117 -0
- package/src/providers/utils/index.ts +27 -0
- package/src/providers/utils/retry.test.ts +140 -0
- package/src/providers/utils/retry.ts +188 -0
- package/src/providers/utils/stream-processor.test.ts +232 -0
- package/src/providers/utils/stream-processor.ts +472 -0
- package/src/providers/utils/tool-processor.test.ts +34 -0
- package/src/providers/utils/tool-processor.ts +111 -0
- package/src/sdk.ts +264 -0
- 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
|
+
});
|