@khanglvm/llm-router 1.3.1 → 2.0.0-beta.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/CHANGELOG.md +39 -0
- package/README.md +337 -41
- package/package.json +19 -3
- package/src/cli/router-module.js +7331 -3805
- package/src/cli/wrangler-toml.js +1 -1
- package/src/cli-entry.js +162 -24
- package/src/node/amp-client-config.js +426 -0
- package/src/node/coding-tool-config.js +763 -0
- package/src/node/config-store.js +49 -18
- package/src/node/instance-state.js +213 -12
- package/src/node/listen-port.js +5 -37
- package/src/node/local-server-settings.js +122 -0
- package/src/node/local-server.js +3 -2
- package/src/node/provider-probe.js +13 -0
- package/src/node/start-command.js +282 -40
- package/src/node/startup-manager.js +64 -29
- package/src/node/web-command.js +106 -0
- package/src/node/web-console-assets.js +26 -0
- package/src/node/web-console-client.js +56 -0
- package/src/node/web-console-dev-assets.js +258 -0
- package/src/node/web-console-server.js +3146 -0
- package/src/node/web-console-styles.generated.js +1 -0
- package/src/node/web-console-ui/config-editor-utils.js +616 -0
- package/src/node/web-console-ui/lib/utils.js +6 -0
- package/src/node/web-console-ui/rate-limit-utils.js +144 -0
- package/src/node/web-console-ui/select-search-utils.js +36 -0
- package/src/runtime/codex-request-transformer.js +46 -5
- package/src/runtime/codex-response-transformer.js +268 -35
- package/src/runtime/config.js +1394 -35
- package/src/runtime/handler/amp-gemini.js +913 -0
- package/src/runtime/handler/amp-response.js +308 -0
- package/src/runtime/handler/amp.js +290 -0
- package/src/runtime/handler/auth.js +17 -2
- package/src/runtime/handler/provider-call.js +168 -50
- package/src/runtime/handler/provider-translation.js +937 -26
- package/src/runtime/handler/request.js +149 -6
- package/src/runtime/handler/route-debug.js +22 -1
- package/src/runtime/handler.js +449 -9
- package/src/runtime/subscription-auth.js +1 -6
- package/src/shared/local-router-defaults.js +62 -0
- package/src/translator/index.js +3 -1
- package/src/translator/request/openai-to-claude.js +217 -6
- package/src/translator/response/openai-to-claude.js +206 -58
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
export const RATE_LIMIT_ALL_MODELS_SELECTOR = "all";
|
|
2
|
+
export const RATE_LIMIT_WINDOW_OPTIONS = ["second", "minute", "hour", "day", "week", "month"];
|
|
3
|
+
|
|
4
|
+
function normalizePositiveInteger(value, fallback = 0) {
|
|
5
|
+
const parsed = Number.parseInt(String(value || ""), 10);
|
|
6
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function normalizeRateLimitWindowUnit(value, fallback = "") {
|
|
10
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
11
|
+
if (RATE_LIMIT_WINDOW_OPTIONS.includes(normalized)) return normalized;
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function pluralizeRateLimitWindowUnit(unit, windowValue = 1) {
|
|
16
|
+
const normalized = String(unit || "").trim().toLowerCase().replace(/s$/, "");
|
|
17
|
+
if (!normalized) return "windows";
|
|
18
|
+
return Number(windowValue) === 1 ? normalized : `${normalized}s`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function normalizeRateLimitModelSelectors(values = []) {
|
|
22
|
+
const normalized = [];
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
|
|
25
|
+
for (const value of (Array.isArray(values) ? values : [values])) {
|
|
26
|
+
const trimmed = String(value || "").trim();
|
|
27
|
+
if (!trimmed) continue;
|
|
28
|
+
if (trimmed.toLowerCase() === RATE_LIMIT_ALL_MODELS_SELECTOR) {
|
|
29
|
+
return [RATE_LIMIT_ALL_MODELS_SELECTOR];
|
|
30
|
+
}
|
|
31
|
+
if (seen.has(trimmed)) continue;
|
|
32
|
+
seen.add(trimmed);
|
|
33
|
+
normalized.push(trimmed);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return normalized;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function buildAutoRateLimitBucketId({ requests, windowValue, windowUnit }) {
|
|
40
|
+
const normalizedRequests = normalizePositiveInteger(requests, 0);
|
|
41
|
+
const normalizedWindowValue = normalizePositiveInteger(windowValue, 0);
|
|
42
|
+
const normalizedWindowUnit = normalizeRateLimitWindowUnit(windowUnit, "");
|
|
43
|
+
if (!normalizedRequests || !normalizedWindowValue || !normalizedWindowUnit) return "";
|
|
44
|
+
return `${normalizedRequests}-req-per-${normalizedWindowValue}-${pluralizeRateLimitWindowUnit(normalizedWindowUnit, normalizedWindowValue)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function formatRateLimitBucketCap(bucket = {}) {
|
|
48
|
+
const requests = normalizePositiveInteger(bucket?.requests ?? bucket?.limit, 0);
|
|
49
|
+
const windowValue = normalizePositiveInteger(bucket?.window?.size ?? bucket?.window?.value ?? bucket?.windowValue, 0);
|
|
50
|
+
const windowUnit = normalizeRateLimitWindowUnit(bucket?.window?.unit ?? bucket?.windowUnit, "");
|
|
51
|
+
if (!requests || !windowValue || !windowUnit) return "Unconfigured";
|
|
52
|
+
return `${requests}/${windowValue} ${pluralizeRateLimitWindowUnit(windowUnit, windowValue)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function validateRateLimitDraftRows(rows = [], {
|
|
56
|
+
knownModelIds = [],
|
|
57
|
+
requireAtLeastOne = true
|
|
58
|
+
} = {}) {
|
|
59
|
+
const normalizedRows = Array.isArray(rows) ? rows : [];
|
|
60
|
+
if (requireAtLeastOne && normalizedRows.length === 0) {
|
|
61
|
+
return "Add at least one rate-limit entity.";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const knownModels = new Set((knownModelIds || []).map((modelId) => String(modelId || "").trim()).filter(Boolean));
|
|
65
|
+
const seenBucketIds = new Set();
|
|
66
|
+
|
|
67
|
+
for (const row of normalizedRows) {
|
|
68
|
+
const models = normalizeRateLimitModelSelectors(row?.models || []);
|
|
69
|
+
if (knownModels.size > 0 && models.some((modelId) => modelId !== RATE_LIMIT_ALL_MODELS_SELECTOR && !knownModels.has(modelId))) {
|
|
70
|
+
return "Rate-limit model selectors must match the provider model ids.";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const requests = normalizePositiveInteger(row?.requests, 0);
|
|
74
|
+
if (!requests) {
|
|
75
|
+
return "Rate-limit requests must be a positive integer.";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const windowValue = normalizePositiveInteger(row?.windowValue, 0);
|
|
79
|
+
if (!windowValue) {
|
|
80
|
+
return "Rate-limit window size must be a positive integer.";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const windowUnit = normalizeRateLimitWindowUnit(row?.windowUnit, "");
|
|
84
|
+
if (!windowUnit) {
|
|
85
|
+
return "Rate-limit window unit is invalid.";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const bucketId = buildAutoRateLimitBucketId({ requests, windowValue, windowUnit });
|
|
89
|
+
if (seenBucketIds.has(bucketId)) {
|
|
90
|
+
return "Duplicate rate-limit entities are not allowed.";
|
|
91
|
+
}
|
|
92
|
+
seenBucketIds.add(bucketId);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return "";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function buildRateLimitBucketsFromDraftRows(rows = [], {
|
|
99
|
+
existingBucketsBySourceId = new Map(),
|
|
100
|
+
fallbackRequests = 60,
|
|
101
|
+
fallbackWindowValue = 1,
|
|
102
|
+
fallbackWindowUnit = "minute"
|
|
103
|
+
} = {}) {
|
|
104
|
+
return (Array.isArray(rows) ? rows : []).map((row, index) => {
|
|
105
|
+
const sourceId = String(row?.sourceId || "").trim();
|
|
106
|
+
const existingBucket = sourceId && existingBucketsBySourceId instanceof Map && existingBucketsBySourceId.has(sourceId)
|
|
107
|
+
? structuredClone(existingBucketsBySourceId.get(sourceId))
|
|
108
|
+
: {};
|
|
109
|
+
const requests = normalizePositiveInteger(
|
|
110
|
+
row?.requests,
|
|
111
|
+
normalizePositiveInteger(existingBucket?.requests ?? existingBucket?.limit, fallbackRequests)
|
|
112
|
+
);
|
|
113
|
+
const windowValue = normalizePositiveInteger(
|
|
114
|
+
row?.windowValue,
|
|
115
|
+
normalizePositiveInteger(existingBucket?.window?.size ?? existingBucket?.window?.value, fallbackWindowValue)
|
|
116
|
+
);
|
|
117
|
+
const windowUnit = normalizeRateLimitWindowUnit(
|
|
118
|
+
row?.windowUnit,
|
|
119
|
+
normalizeRateLimitWindowUnit(existingBucket?.window?.unit, fallbackWindowUnit) || fallbackWindowUnit
|
|
120
|
+
);
|
|
121
|
+
const models = normalizeRateLimitModelSelectors(row?.models || []);
|
|
122
|
+
const effectiveModels = models.length > 0 ? models : [RATE_LIMIT_ALL_MODELS_SELECTOR];
|
|
123
|
+
|
|
124
|
+
const bucket = {
|
|
125
|
+
...existingBucket,
|
|
126
|
+
id: buildAutoRateLimitBucketId({ requests, windowValue, windowUnit }) || `rate-limit-${index + 1}`,
|
|
127
|
+
models: effectiveModels,
|
|
128
|
+
requests,
|
|
129
|
+
window: {
|
|
130
|
+
...(existingBucket?.window && typeof existingBucket.window === "object" && !Array.isArray(existingBucket.window) ? existingBucket.window : {}),
|
|
131
|
+
size: windowValue,
|
|
132
|
+
unit: windowUnit
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
delete bucket.name;
|
|
137
|
+
delete bucket.limit;
|
|
138
|
+
if (bucket.window && typeof bucket.window === "object") {
|
|
139
|
+
delete bucket.window.value;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return bucket;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
function normalizeSelectSearchText(value) {
|
|
2
|
+
return String(value || "").trim().toLowerCase();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function hasSelectSearchQuery(value) {
|
|
6
|
+
return normalizeSelectSearchText(value).length > 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getSelectSearchKey(event) {
|
|
10
|
+
const key = String(event?.key || "");
|
|
11
|
+
if (!key || key.length !== 1) return "";
|
|
12
|
+
if (event?.ctrlKey || event?.metaKey || event?.altKey) return "";
|
|
13
|
+
return key.trim() ? key : "";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function optionMatchesSelectQuery(option = {}, query = "") {
|
|
17
|
+
const normalizedQuery = normalizeSelectSearchText(query);
|
|
18
|
+
if (!normalizedQuery) return true;
|
|
19
|
+
|
|
20
|
+
const haystack = [
|
|
21
|
+
option?.label,
|
|
22
|
+
option?.value,
|
|
23
|
+
option?.hint,
|
|
24
|
+
option?.textValue,
|
|
25
|
+
option?.searchText
|
|
26
|
+
]
|
|
27
|
+
.map((entry) => String(entry || "").toLowerCase())
|
|
28
|
+
.join(" ");
|
|
29
|
+
|
|
30
|
+
return haystack.includes(normalizedQuery);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function filterSelectOptions(options = [], query = "") {
|
|
34
|
+
const normalizedOptions = Array.isArray(options) ? options : [];
|
|
35
|
+
return normalizedOptions.filter((option) => optionMatchesSelectQuery(option, query));
|
|
36
|
+
}
|
|
@@ -27,12 +27,14 @@ export function transformRequestForCodex(body) {
|
|
|
27
27
|
? { ...body }
|
|
28
28
|
: {};
|
|
29
29
|
|
|
30
|
-
const instructions = typeof transformed.instructions === 'string'
|
|
31
|
-
? transformed.instructions.trim()
|
|
32
|
-
: '';
|
|
33
30
|
const reasoning = normalizeReasoningConfig(transformed.reasoning, transformed.reasoning_effort);
|
|
34
31
|
const include = normalizeIncludeList(transformed.include, reasoning);
|
|
35
|
-
const
|
|
32
|
+
const resolvedInput = resolveResponseInput(transformed);
|
|
33
|
+
const extractedGuidance = extractLeadingInstructionMessages(resolvedInput);
|
|
34
|
+
const instructions = joinInstructionText(
|
|
35
|
+
typeof transformed.instructions === 'string' ? transformed.instructions.trim() : '',
|
|
36
|
+
extractedGuidance.instructions
|
|
37
|
+
);
|
|
36
38
|
const tools = Array.isArray(transformed.tools)
|
|
37
39
|
? transformed.tools.map(normalizeToolDefinitionForResponses).filter(Boolean)
|
|
38
40
|
: [];
|
|
@@ -40,7 +42,7 @@ export function transformRequestForCodex(body) {
|
|
|
40
42
|
const output = {
|
|
41
43
|
model: transformed.model,
|
|
42
44
|
instructions: instructions || DEFAULT_CODEX_INSTRUCTIONS,
|
|
43
|
-
input,
|
|
45
|
+
input: extractedGuidance.input,
|
|
44
46
|
tools,
|
|
45
47
|
tool_choice: normalizeToolChoiceForResponses(transformed.tool_choice),
|
|
46
48
|
parallel_tool_calls: Boolean(transformed.parallel_tool_calls),
|
|
@@ -68,6 +70,13 @@ function hasUsableInput(input) {
|
|
|
68
70
|
return Array.isArray(input) && input.length > 0;
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
function joinInstructionText(...parts) {
|
|
74
|
+
return parts
|
|
75
|
+
.map((value) => (typeof value === 'string' ? value.trim() : ''))
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.join('\n\n');
|
|
78
|
+
}
|
|
79
|
+
|
|
71
80
|
function resolveResponseInput(transformed) {
|
|
72
81
|
if (hasUsableInput(transformed.input)) return transformed.input;
|
|
73
82
|
if (typeof transformed.input === 'string' && transformed.input.trim()) {
|
|
@@ -83,6 +92,38 @@ function resolveResponseInput(transformed) {
|
|
|
83
92
|
return [];
|
|
84
93
|
}
|
|
85
94
|
|
|
95
|
+
function isInstructionMessageItem(item) {
|
|
96
|
+
if (!item || typeof item !== 'object') return false;
|
|
97
|
+
if (item.type !== 'message') return false;
|
|
98
|
+
const role = normalizeMessageRole(item.role);
|
|
99
|
+
return role === 'system' || role === 'developer';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function extractLeadingInstructionMessages(input) {
|
|
103
|
+
if (!Array.isArray(input) || input.length === 0) {
|
|
104
|
+
return {
|
|
105
|
+
instructions: '',
|
|
106
|
+
input: []
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const instructions = [];
|
|
111
|
+
let index = 0;
|
|
112
|
+
|
|
113
|
+
while (index < input.length) {
|
|
114
|
+
const item = input[index];
|
|
115
|
+
if (!isInstructionMessageItem(item)) break;
|
|
116
|
+
const text = normalizeMessageContentToText(item.content);
|
|
117
|
+
if (text) instructions.push(text);
|
|
118
|
+
index += 1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
instructions: joinInstructionText(...instructions),
|
|
123
|
+
input: index > 0 ? input.slice(index) : input
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
86
127
|
function normalizeIncludeList(rawInclude, reasoning) {
|
|
87
128
|
const include = Array.isArray(rawInclude)
|
|
88
129
|
? rawInclude.map((value) => String(value || '').trim()).filter(Boolean)
|
|
@@ -133,6 +133,49 @@ function ensureAssistantRoleChunk(state, chunks) {
|
|
|
133
133
|
chunks.push(makeOpenAIChunk(state, { role: 'assistant' }, null));
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
function commonPrefixLength(left, right) {
|
|
137
|
+
const leftText = typeof left === 'string' ? left : '';
|
|
138
|
+
const rightText = typeof right === 'string' ? right : '';
|
|
139
|
+
const limit = Math.min(leftText.length, rightText.length);
|
|
140
|
+
let index = 0;
|
|
141
|
+
|
|
142
|
+
while (index < limit && leftText[index] === rightText[index]) {
|
|
143
|
+
index += 1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return index;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getMissingSuffix(emittedText, finalText) {
|
|
150
|
+
const emitted = typeof emittedText === 'string' ? emittedText : '';
|
|
151
|
+
const finalValue = typeof finalText === 'string' ? finalText : '';
|
|
152
|
+
|
|
153
|
+
if (!finalValue) return '';
|
|
154
|
+
if (!emitted) return finalValue;
|
|
155
|
+
if (finalValue.startsWith(emitted)) {
|
|
156
|
+
return finalValue.slice(emitted.length);
|
|
157
|
+
}
|
|
158
|
+
if (emitted.startsWith(finalValue)) {
|
|
159
|
+
return '';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const prefixLength = commonPrefixLength(emitted, finalValue);
|
|
163
|
+
if (prefixLength <= 0) return '';
|
|
164
|
+
return finalValue.slice(prefixLength);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function parseStreamBlock(block) {
|
|
168
|
+
const normalized = String(block || '').trim();
|
|
169
|
+
if (!normalized) return null;
|
|
170
|
+
if (!normalized.includes('data:') && !normalized.includes('event:')) {
|
|
171
|
+
return {
|
|
172
|
+
eventType: '',
|
|
173
|
+
data: normalized
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return parseSseBlock(normalized);
|
|
177
|
+
}
|
|
178
|
+
|
|
136
179
|
function parseSseBlock(block) {
|
|
137
180
|
let eventType = '';
|
|
138
181
|
const dataLines = [];
|
|
@@ -185,7 +228,7 @@ export function extractCodexFinalResponseFromText(rawText) {
|
|
|
185
228
|
|
|
186
229
|
for (const block of blocks) {
|
|
187
230
|
if (!block || !block.trim()) continue;
|
|
188
|
-
const parsedBlock =
|
|
231
|
+
const parsedBlock = parseStreamBlock(block);
|
|
189
232
|
if (!parsedBlock.data || parsedBlock.data === '[DONE]') continue;
|
|
190
233
|
|
|
191
234
|
let payload;
|
|
@@ -241,12 +284,114 @@ function updateStateFromResponse(state, response, fallbackModel) {
|
|
|
241
284
|
}
|
|
242
285
|
}
|
|
243
286
|
|
|
287
|
+
function extractAssistantOutputText(item) {
|
|
288
|
+
if (!item || item.type !== 'message' || item.role !== 'assistant' || !Array.isArray(item.content)) {
|
|
289
|
+
return '';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const textParts = [];
|
|
293
|
+
for (const contentPart of item.content) {
|
|
294
|
+
if (!contentPart || typeof contentPart !== 'object') continue;
|
|
295
|
+
if (contentPart.type === 'output_text' && typeof contentPart.text === 'string') {
|
|
296
|
+
textParts.push(contentPart.text);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (contentPart.type === 'refusal' && typeof contentPart.refusal === 'string') {
|
|
300
|
+
textParts.push(contentPart.refusal);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return textParts.join('');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function emitFallbackTextChunk(state, item, chunks) {
|
|
308
|
+
const text = extractAssistantOutputText(item);
|
|
309
|
+
if (!text) return;
|
|
310
|
+
|
|
311
|
+
const itemId = typeof item?.id === 'string' ? item.id.trim() : '';
|
|
312
|
+
const missingText = itemId
|
|
313
|
+
? getMissingSuffix(state.textOutputByItemId.get(itemId) || '', text)
|
|
314
|
+
: (state.hasTextOutput ? '' : text);
|
|
315
|
+
if (!missingText) return;
|
|
316
|
+
|
|
317
|
+
ensureAssistantRoleChunk(state, chunks);
|
|
318
|
+
chunks.push(makeOpenAIChunk(state, { content: missingText }, null));
|
|
319
|
+
if (itemId) {
|
|
320
|
+
state.textOutputItemIds.add(itemId);
|
|
321
|
+
state.textOutputByItemId.set(itemId, `${state.textOutputByItemId.get(itemId) || ''}${missingText}`);
|
|
322
|
+
}
|
|
323
|
+
state.hasTextOutput = true;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function emitFallbackToolCallChunks(state, item, outputIndex, chunks) {
|
|
327
|
+
if (!item || item.type !== 'function_call') return;
|
|
328
|
+
|
|
329
|
+
ensureAssistantRoleChunk(state, chunks);
|
|
330
|
+
state.hasToolCalls = true;
|
|
331
|
+
|
|
332
|
+
const toolIndex = resolveToolIndex(state, {
|
|
333
|
+
output_index: outputIndex,
|
|
334
|
+
item_id: typeof item.id === 'string' && item.id.trim() ? item.id.trim() : undefined
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
if (!state.toolCallStartSentByIndex.has(toolIndex)) {
|
|
338
|
+
chunks.push(makeOpenAIChunk(state, {
|
|
339
|
+
tool_calls: [
|
|
340
|
+
{
|
|
341
|
+
index: toolIndex,
|
|
342
|
+
id: String(item.call_id || item.id || `call_${toolIndex}`),
|
|
343
|
+
type: 'function',
|
|
344
|
+
function: {
|
|
345
|
+
name: String(item.name || 'tool'),
|
|
346
|
+
arguments: ''
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
]
|
|
350
|
+
}, null));
|
|
351
|
+
state.toolCallStartSentByIndex.add(toolIndex);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const argumentsText = typeof item.arguments === 'string' ? item.arguments : '';
|
|
355
|
+
const missingArguments = getMissingSuffix(state.toolCallArgumentsByIndex.get(toolIndex) || '', argumentsText);
|
|
356
|
+
if (missingArguments) {
|
|
357
|
+
chunks.push(makeOpenAIChunk(state, {
|
|
358
|
+
tool_calls: [
|
|
359
|
+
{
|
|
360
|
+
index: toolIndex,
|
|
361
|
+
function: {
|
|
362
|
+
arguments: missingArguments
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
]
|
|
366
|
+
}, null));
|
|
367
|
+
state.toolCallArgumentsSeenByIndex.add(toolIndex);
|
|
368
|
+
state.toolCallArgumentsByIndex.set(toolIndex, `${state.toolCallArgumentsByIndex.get(toolIndex) || ''}${missingArguments}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function emitResponseOutputFallbacks(state, response, chunks) {
|
|
373
|
+
const outputItems = Array.isArray(response?.output) ? response.output : [];
|
|
374
|
+
for (let index = 0; index < outputItems.length; index += 1) {
|
|
375
|
+
const item = outputItems[index];
|
|
376
|
+
if (!item || typeof item !== 'object') continue;
|
|
377
|
+
|
|
378
|
+
if (item.type === 'message' && item.role === 'assistant') {
|
|
379
|
+
emitFallbackTextChunk(state, item, chunks);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (item.type === 'function_call') {
|
|
384
|
+
emitFallbackToolCallChunks(state, item, index, chunks);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
244
389
|
function eventToOpenAIChunks(event, state, { fallbackModel = 'unknown' } = {}) {
|
|
245
390
|
if (!event || typeof event !== 'object') return [];
|
|
246
391
|
const type = String(event.type || '').trim();
|
|
247
392
|
const chunks = [];
|
|
248
393
|
|
|
249
|
-
if (type === 'response.created' || type === 'response.in_progress'
|
|
394
|
+
if (type === 'response.created' || type === 'response.in_progress') {
|
|
250
395
|
updateStateFromResponse(state, event.response, fallbackModel);
|
|
251
396
|
return chunks;
|
|
252
397
|
}
|
|
@@ -259,6 +404,7 @@ function eventToOpenAIChunks(event, state, { fallbackModel = 'unknown' } = {}) {
|
|
|
259
404
|
ensureAssistantRoleChunk(state, chunks);
|
|
260
405
|
const toolIndex = resolveToolIndex(state, event);
|
|
261
406
|
state.hasToolCalls = true;
|
|
407
|
+
state.toolCallStartSentByIndex.add(toolIndex);
|
|
262
408
|
chunks.push(makeOpenAIChunk(state, {
|
|
263
409
|
tool_calls: [
|
|
264
410
|
{
|
|
@@ -280,34 +426,85 @@ function eventToOpenAIChunks(event, state, { fallbackModel = 'unknown' } = {}) {
|
|
|
280
426
|
return chunks;
|
|
281
427
|
}
|
|
282
428
|
|
|
429
|
+
if (type === 'response.reasoning_summary_text.delta') {
|
|
430
|
+
ensureAssistantRoleChunk(state, chunks);
|
|
431
|
+
chunks.push(makeOpenAIChunk(state, { reasoning_content: String(event.delta || '') }, null));
|
|
432
|
+
return chunks;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (type === 'response.reasoning_summary_text.done') {
|
|
436
|
+
if (typeof event.text === 'string' && event.text) {
|
|
437
|
+
ensureAssistantRoleChunk(state, chunks);
|
|
438
|
+
chunks.push(makeOpenAIChunk(state, { reasoning_content: event.text }, null));
|
|
439
|
+
}
|
|
440
|
+
return chunks;
|
|
441
|
+
}
|
|
442
|
+
|
|
283
443
|
if (type === 'response.output_text.delta') {
|
|
444
|
+
const deltaText = String(event.delta || '');
|
|
445
|
+
if (!deltaText) return chunks;
|
|
284
446
|
ensureAssistantRoleChunk(state, chunks);
|
|
285
447
|
if (typeof event.item_id === 'string' && event.item_id.trim()) {
|
|
286
|
-
|
|
448
|
+
const itemId = event.item_id.trim();
|
|
449
|
+
state.textOutputItemIds.add(itemId);
|
|
450
|
+
state.textOutputByItemId.set(itemId, `${state.textOutputByItemId.get(itemId) || ''}${deltaText}`);
|
|
287
451
|
}
|
|
288
|
-
|
|
452
|
+
state.hasTextOutput = true;
|
|
453
|
+
chunks.push(makeOpenAIChunk(state, { content: deltaText }, null));
|
|
289
454
|
return chunks;
|
|
290
455
|
}
|
|
291
456
|
|
|
292
457
|
if (type === 'response.output_text.done') {
|
|
293
458
|
const itemId = typeof event.item_id === 'string' ? event.item_id.trim() : '';
|
|
294
|
-
|
|
459
|
+
const finalText = typeof event.text === 'string' ? event.text : '';
|
|
460
|
+
const missingText = itemId
|
|
461
|
+
? getMissingSuffix(state.textOutputByItemId.get(itemId) || '', finalText)
|
|
462
|
+
: (state.hasTextOutput ? '' : finalText);
|
|
463
|
+
if (missingText) {
|
|
295
464
|
ensureAssistantRoleChunk(state, chunks);
|
|
296
|
-
chunks.push(makeOpenAIChunk(state, { content:
|
|
465
|
+
chunks.push(makeOpenAIChunk(state, { content: missingText }, null));
|
|
466
|
+
if (itemId) {
|
|
467
|
+
state.textOutputItemIds.add(itemId);
|
|
468
|
+
state.textOutputByItemId.set(itemId, `${state.textOutputByItemId.get(itemId) || ''}${missingText}`);
|
|
469
|
+
}
|
|
470
|
+
state.hasTextOutput = true;
|
|
297
471
|
}
|
|
298
472
|
return chunks;
|
|
299
473
|
}
|
|
300
474
|
|
|
475
|
+
if (type === 'response.content_part.done') {
|
|
476
|
+
const itemId = typeof event.item_id === 'string' ? event.item_id.trim() : '';
|
|
477
|
+
const finalText = event.part?.type === 'output_text' && typeof event.part?.text === 'string'
|
|
478
|
+
? event.part.text
|
|
479
|
+
: '';
|
|
480
|
+
const missingText = itemId
|
|
481
|
+
? getMissingSuffix(state.textOutputByItemId.get(itemId) || '', finalText)
|
|
482
|
+
: (state.hasTextOutput ? '' : finalText);
|
|
483
|
+
if (!missingText) return chunks;
|
|
484
|
+
ensureAssistantRoleChunk(state, chunks);
|
|
485
|
+
chunks.push(makeOpenAIChunk(state, { content: missingText }, null));
|
|
486
|
+
if (itemId) {
|
|
487
|
+
state.textOutputItemIds.add(itemId);
|
|
488
|
+
state.textOutputByItemId.set(itemId, `${state.textOutputByItemId.get(itemId) || ''}${missingText}`);
|
|
489
|
+
}
|
|
490
|
+
state.hasTextOutput = true;
|
|
491
|
+
return chunks;
|
|
492
|
+
}
|
|
493
|
+
|
|
301
494
|
if (type === 'response.function_call_arguments.delta') {
|
|
495
|
+
const deltaArguments = String(event.delta || '');
|
|
496
|
+
if (!deltaArguments) return chunks;
|
|
302
497
|
ensureAssistantRoleChunk(state, chunks);
|
|
303
498
|
const toolIndex = resolveToolIndex(state, event);
|
|
304
499
|
state.hasToolCalls = true;
|
|
500
|
+
state.toolCallArgumentsSeenByIndex.add(toolIndex);
|
|
501
|
+
state.toolCallArgumentsByIndex.set(toolIndex, `${state.toolCallArgumentsByIndex.get(toolIndex) || ''}${deltaArguments}`);
|
|
305
502
|
chunks.push(makeOpenAIChunk(state, {
|
|
306
503
|
tool_calls: [
|
|
307
504
|
{
|
|
308
505
|
index: toolIndex,
|
|
309
506
|
function: {
|
|
310
|
-
arguments:
|
|
507
|
+
arguments: deltaArguments
|
|
311
508
|
}
|
|
312
509
|
}
|
|
313
510
|
]
|
|
@@ -316,15 +513,20 @@ function eventToOpenAIChunks(event, state, { fallbackModel = 'unknown' } = {}) {
|
|
|
316
513
|
}
|
|
317
514
|
|
|
318
515
|
if (type === 'response.function_call_arguments.done') {
|
|
319
|
-
ensureAssistantRoleChunk(state, chunks);
|
|
320
516
|
const toolIndex = resolveToolIndex(state, event);
|
|
517
|
+
const finalArguments = String(event.arguments || '');
|
|
518
|
+
const missingArguments = getMissingSuffix(state.toolCallArgumentsByIndex.get(toolIndex) || '', finalArguments);
|
|
519
|
+
if (!missingArguments) return chunks;
|
|
520
|
+
ensureAssistantRoleChunk(state, chunks);
|
|
321
521
|
state.hasToolCalls = true;
|
|
522
|
+
state.toolCallArgumentsSeenByIndex.add(toolIndex);
|
|
523
|
+
state.toolCallArgumentsByIndex.set(toolIndex, `${state.toolCallArgumentsByIndex.get(toolIndex) || ''}${missingArguments}`);
|
|
322
524
|
chunks.push(makeOpenAIChunk(state, {
|
|
323
525
|
tool_calls: [
|
|
324
526
|
{
|
|
325
527
|
index: toolIndex,
|
|
326
528
|
function: {
|
|
327
|
-
arguments:
|
|
529
|
+
arguments: missingArguments
|
|
328
530
|
}
|
|
329
531
|
}
|
|
330
532
|
]
|
|
@@ -332,8 +534,26 @@ function eventToOpenAIChunks(event, state, { fallbackModel = 'unknown' } = {}) {
|
|
|
332
534
|
return chunks;
|
|
333
535
|
}
|
|
334
536
|
|
|
335
|
-
if (type === 'response.
|
|
537
|
+
if (type === 'response.output_item.done') {
|
|
336
538
|
updateStateFromResponse(state, event.response, fallbackModel);
|
|
539
|
+
const item = event.item;
|
|
540
|
+
if (!item || typeof item !== 'object') return chunks;
|
|
541
|
+
|
|
542
|
+
if (item.type === 'message' && item.role === 'assistant') {
|
|
543
|
+
emitFallbackTextChunk(state, item, chunks);
|
|
544
|
+
return chunks;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (item.type === 'function_call') {
|
|
548
|
+
emitFallbackToolCallChunks(state, item, Number.isFinite(event.output_index) ? Number(event.output_index) : 0, chunks);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return chunks;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (type === 'response.completed' || type === 'response.failed' || type === 'response.incomplete') {
|
|
555
|
+
updateStateFromResponse(state, event.response, fallbackModel);
|
|
556
|
+
emitResponseOutputFallbacks(state, event.response, chunks);
|
|
337
557
|
ensureAssistantRoleChunk(state, chunks);
|
|
338
558
|
const responseUsage = toOpenAIUsage(event.response?.usage);
|
|
339
559
|
const hasResponseToolCalls = Array.isArray(event.response?.output)
|
|
@@ -370,11 +590,43 @@ export function handleCodexStreamToOpenAI(response, { fallbackModel = 'unknown'
|
|
|
370
590
|
toolCallByOutputIndex: new Map(),
|
|
371
591
|
toolCallByItemId: new Map(),
|
|
372
592
|
nextToolCallIndex: 0,
|
|
373
|
-
|
|
593
|
+
toolCallStartSentByIndex: new Set(),
|
|
594
|
+
toolCallArgumentsSeenByIndex: new Set(),
|
|
595
|
+
toolCallArgumentsByIndex: new Map(),
|
|
596
|
+
textOutputItemIds: new Set(),
|
|
597
|
+
textOutputByItemId: new Map(),
|
|
598
|
+
hasTextOutput: false
|
|
374
599
|
};
|
|
375
600
|
|
|
376
601
|
let buffer = '';
|
|
377
602
|
|
|
603
|
+
function processBlock(block, controller) {
|
|
604
|
+
if (!block || !block.trim()) return;
|
|
605
|
+
|
|
606
|
+
const parsedBlock = parseStreamBlock(block);
|
|
607
|
+
if (!parsedBlock.data) return;
|
|
608
|
+
|
|
609
|
+
if (parsedBlock.data === '[DONE]') {
|
|
610
|
+
if (!state.doneSent) {
|
|
611
|
+
state.doneSent = true;
|
|
612
|
+
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
|
|
613
|
+
}
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
let payload;
|
|
618
|
+
try {
|
|
619
|
+
payload = JSON.parse(parsedBlock.data);
|
|
620
|
+
} catch {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const chunks = eventToOpenAIChunks(payload, state, { fallbackModel });
|
|
625
|
+
for (const translated of chunks) {
|
|
626
|
+
controller.enqueue(encoder.encode(serializeOpenAIChunk(translated)));
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
378
630
|
const transformStream = new TransformStream({
|
|
379
631
|
transform(chunk, controller) {
|
|
380
632
|
buffer += decoder.decode(chunk, { stream: true }).replace(/\r\n/g, '\n');
|
|
@@ -383,34 +635,15 @@ export function handleCodexStreamToOpenAI(response, { fallbackModel = 'unknown'
|
|
|
383
635
|
while ((boundaryIndex = buffer.indexOf('\n\n')) >= 0) {
|
|
384
636
|
const block = buffer.slice(0, boundaryIndex);
|
|
385
637
|
buffer = buffer.slice(boundaryIndex + 2);
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const parsedBlock = parseSseBlock(block);
|
|
389
|
-
if (!parsedBlock.data) continue;
|
|
390
|
-
|
|
391
|
-
if (parsedBlock.data === '[DONE]') {
|
|
392
|
-
if (!state.doneSent) {
|
|
393
|
-
state.doneSent = true;
|
|
394
|
-
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
|
|
395
|
-
}
|
|
396
|
-
continue;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
let payload;
|
|
400
|
-
try {
|
|
401
|
-
payload = JSON.parse(parsedBlock.data);
|
|
402
|
-
} catch {
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const chunks = eventToOpenAIChunks(payload, state, { fallbackModel });
|
|
407
|
-
for (const translated of chunks) {
|
|
408
|
-
controller.enqueue(encoder.encode(serializeOpenAIChunk(translated)));
|
|
409
|
-
}
|
|
638
|
+
processBlock(block, controller);
|
|
410
639
|
}
|
|
411
640
|
},
|
|
412
641
|
|
|
413
642
|
flush(controller) {
|
|
643
|
+
const remainder = buffer.trim();
|
|
644
|
+
if (remainder) {
|
|
645
|
+
processBlock(remainder, controller);
|
|
646
|
+
}
|
|
414
647
|
if (state.doneSent) return;
|
|
415
648
|
if (!state.roleSent) {
|
|
416
649
|
controller.enqueue(encoder.encode(serializeOpenAIChunk(makeOpenAIChunk(state, { role: 'assistant' }, null))));
|