@kevin0181/rcodex 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +160 -0
  3. package/dist/commands/doctor.d.ts +2 -0
  4. package/dist/commands/doctor.d.ts.map +1 -0
  5. package/dist/commands/doctor.js +114 -0
  6. package/dist/commands/doctor.js.map +1 -0
  7. package/dist/commands/gateway-daemon.d.ts +2 -0
  8. package/dist/commands/gateway-daemon.d.ts.map +1 -0
  9. package/dist/commands/gateway-daemon.js +22 -0
  10. package/dist/commands/gateway-daemon.js.map +1 -0
  11. package/dist/commands/launch.d.ts +2 -0
  12. package/dist/commands/launch.d.ts.map +1 -0
  13. package/dist/commands/launch.js +129 -0
  14. package/dist/commands/launch.js.map +1 -0
  15. package/dist/commands/migrate.d.ts +4 -0
  16. package/dist/commands/migrate.d.ts.map +1 -0
  17. package/dist/commands/migrate.js +137 -0
  18. package/dist/commands/migrate.js.map +1 -0
  19. package/dist/commands/setup.d.ts +2 -0
  20. package/dist/commands/setup.d.ts.map +1 -0
  21. package/dist/commands/setup.js +78 -0
  22. package/dist/commands/setup.js.map +1 -0
  23. package/dist/commands/stop.d.ts +2 -0
  24. package/dist/commands/stop.d.ts.map +1 -0
  25. package/dist/commands/stop.js +20 -0
  26. package/dist/commands/stop.js.map +1 -0
  27. package/dist/commands/switch.d.ts +4 -0
  28. package/dist/commands/switch.d.ts.map +1 -0
  29. package/dist/commands/switch.js +78 -0
  30. package/dist/commands/switch.js.map +1 -0
  31. package/dist/commands/sync.d.ts +3 -0
  32. package/dist/commands/sync.d.ts.map +1 -0
  33. package/dist/commands/sync.js +107 -0
  34. package/dist/commands/sync.js.map +1 -0
  35. package/dist/core/codex.d.ts +6 -0
  36. package/dist/core/codex.d.ts.map +1 -0
  37. package/dist/core/codex.js +123 -0
  38. package/dist/core/codex.js.map +1 -0
  39. package/dist/core/config.d.ts +6 -0
  40. package/dist/core/config.d.ts.map +1 -0
  41. package/dist/core/config.js +68 -0
  42. package/dist/core/config.js.map +1 -0
  43. package/dist/core/constants.d.ts +4 -0
  44. package/dist/core/constants.d.ts.map +1 -0
  45. package/dist/core/constants.js +7 -0
  46. package/dist/core/constants.js.map +1 -0
  47. package/dist/core/ollama.d.ts +3 -0
  48. package/dist/core/ollama.d.ts.map +1 -0
  49. package/dist/core/ollama.js +20 -0
  50. package/dist/core/ollama.js.map +1 -0
  51. package/dist/gateway/auth.d.ts +58 -0
  52. package/dist/gateway/auth.d.ts.map +1 -0
  53. package/dist/gateway/auth.js +248 -0
  54. package/dist/gateway/auth.js.map +1 -0
  55. package/dist/gateway/providers/anthropic.d.ts +15 -0
  56. package/dist/gateway/providers/anthropic.d.ts.map +1 -0
  57. package/dist/gateway/providers/anthropic.js +122 -0
  58. package/dist/gateway/providers/anthropic.js.map +1 -0
  59. package/dist/gateway/providers/antigravity-oauth-flow.d.ts +21 -0
  60. package/dist/gateway/providers/antigravity-oauth-flow.d.ts.map +1 -0
  61. package/dist/gateway/providers/antigravity-oauth-flow.js +231 -0
  62. package/dist/gateway/providers/antigravity-oauth-flow.js.map +1 -0
  63. package/dist/gateway/providers/antigravity.d.ts +5 -0
  64. package/dist/gateway/providers/antigravity.d.ts.map +1 -0
  65. package/dist/gateway/providers/antigravity.js +111 -0
  66. package/dist/gateway/providers/antigravity.js.map +1 -0
  67. package/dist/gateway/providers/claude-oauth-flow.d.ts +16 -0
  68. package/dist/gateway/providers/claude-oauth-flow.d.ts.map +1 -0
  69. package/dist/gateway/providers/claude-oauth-flow.js +178 -0
  70. package/dist/gateway/providers/claude-oauth-flow.js.map +1 -0
  71. package/dist/gateway/providers/copilot.d.ts +19 -0
  72. package/dist/gateway/providers/copilot.d.ts.map +1 -0
  73. package/dist/gateway/providers/copilot.js +141 -0
  74. package/dist/gateway/providers/copilot.js.map +1 -0
  75. package/dist/gateway/providers/google.d.ts +12 -0
  76. package/dist/gateway/providers/google.d.ts.map +1 -0
  77. package/dist/gateway/providers/google.js +58 -0
  78. package/dist/gateway/providers/google.js.map +1 -0
  79. package/dist/gateway/providers/ollama.d.ts +6 -0
  80. package/dist/gateway/providers/ollama.d.ts.map +1 -0
  81. package/dist/gateway/providers/ollama.js +54 -0
  82. package/dist/gateway/providers/ollama.js.map +1 -0
  83. package/dist/gateway/providers/openai-oauth-flow.d.ts +15 -0
  84. package/dist/gateway/providers/openai-oauth-flow.d.ts.map +1 -0
  85. package/dist/gateway/providers/openai-oauth-flow.js +149 -0
  86. package/dist/gateway/providers/openai-oauth-flow.js.map +1 -0
  87. package/dist/gateway/providers/openai.d.ts +8 -0
  88. package/dist/gateway/providers/openai.d.ts.map +1 -0
  89. package/dist/gateway/providers/openai.js +193 -0
  90. package/dist/gateway/providers/openai.js.map +1 -0
  91. package/dist/gateway/proxy.d.ts +119 -0
  92. package/dist/gateway/proxy.d.ts.map +1 -0
  93. package/dist/gateway/proxy.js +1949 -0
  94. package/dist/gateway/proxy.js.map +1 -0
  95. package/dist/gateway/server.d.ts +6 -0
  96. package/dist/gateway/server.d.ts.map +1 -0
  97. package/dist/gateway/server.js +890 -0
  98. package/dist/gateway/server.js.map +1 -0
  99. package/dist/gateway/ui.d.ts +2 -0
  100. package/dist/gateway/ui.d.ts.map +1 -0
  101. package/dist/gateway/ui.js +1748 -0
  102. package/dist/gateway/ui.js.map +1 -0
  103. package/dist/index.d.ts +3 -0
  104. package/dist/index.d.ts.map +1 -0
  105. package/dist/index.js +78 -0
  106. package/dist/index.js.map +1 -0
  107. package/dist/types/index.d.ts +26 -0
  108. package/dist/types/index.d.ts.map +1 -0
  109. package/dist/types/index.js +2 -0
  110. package/dist/types/index.js.map +1 -0
  111. package/dist/utils/logger.d.ts +10 -0
  112. package/dist/utils/logger.d.ts.map +1 -0
  113. package/dist/utils/logger.js +25 -0
  114. package/dist/utils/logger.js.map +1 -0
  115. package/dist/utils/paths.d.ts +4 -0
  116. package/dist/utils/paths.d.ts.map +1 -0
  117. package/dist/utils/paths.js +22 -0
  118. package/dist/utils/paths.js.map +1 -0
  119. package/dist/utils/shell.d.ts +8 -0
  120. package/dist/utils/shell.d.ts.map +1 -0
  121. package/dist/utils/shell.js +27 -0
  122. package/dist/utils/shell.js.map +1 -0
  123. package/dist/utils/updates.d.ts +2 -0
  124. package/dist/utils/updates.d.ts.map +1 -0
  125. package/dist/utils/updates.js +83 -0
  126. package/dist/utils/updates.js.map +1 -0
  127. package/package.json +61 -0
@@ -0,0 +1,1949 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+ import { accountToProviderAuth, updateAccountOAuth, loadConfig, saveConfig } from "./auth.js";
5
+ import { isAnthropicModel, callAnthropic, supportsThinking } from "./providers/anthropic.js";
6
+ import { isOpenAIModel, callOpenAI, callOpenAIAndParse, callCodexAPI } from "./providers/openai.js";
7
+ import { isGoogleModel, callGoogle } from "./providers/google.js";
8
+ import { isOllamaModel, callOllama } from "./providers/ollama.js";
9
+ import { refreshOpenAIAccessToken } from "./providers/openai-oauth-flow.js";
10
+ import { refreshClaudeAccessToken } from "./providers/claude-oauth-flow.js";
11
+ import { isAntigravityModel, callAntigravity } from "./providers/antigravity.js";
12
+ import { refreshAntigravityAccessToken, loadAntigravityProject } from "./providers/antigravity-oauth-flow.js";
13
+ import { callCopilot } from "./providers/copilot.js";
14
+ const RCODEX_DIR = join(homedir(), ".rcodex");
15
+ const REQUESTS_PATH = join(RCODEX_DIR, "requests.jsonl");
16
+ // Start of today in local time (resets at user's system midnight, not a rolling 24h)
17
+ function todayMidnight() {
18
+ const d = new Date();
19
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
20
+ }
21
+ // Timer: flush entries from the previous day at each local midnight
22
+ function scheduleMidnightReset() {
23
+ const msUntilMidnight = todayMidnight() + 86_400_000 - Date.now();
24
+ setTimeout(() => {
25
+ const cutoff = todayMidnight();
26
+ while (requestLog.length > 0 && requestLog[0].ts < cutoff)
27
+ requestLog.shift();
28
+ try {
29
+ writeFileSync(REQUESTS_PATH, requestLog.map(e => JSON.stringify(e)).join("\n") + (requestLog.length ? "\n" : ""), "utf-8");
30
+ }
31
+ catch { /* ignore */ }
32
+ scheduleMidnightReset(); // reschedule for the next midnight
33
+ }, msUntilMidnight + 500);
34
+ }
35
+ // KST timestamp for log lines (UTC+9)
36
+ function kst() {
37
+ return new Date(Date.now() + 9 * 60 * 60 * 1000).toISOString().slice(0, 19).replace("T", " ");
38
+ }
39
+ export function glog(msg) { console.log(`[${kst()}] ${msg}`); }
40
+ export function gwarn(msg) { console.warn(`[${kst()}] ${msg}`); }
41
+ export function gerr(msg) { console.error(`[${kst()}] ${msg}`); }
42
+ // ?�?� Tool format conversion ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
43
+ const COMPUTER_USE_TYPES = new Set(["computer_use", "computer-preview", "computer_20241022"]);
44
+ function toAnthropicTool(tool) {
45
+ const toolType = tool.type;
46
+ if (toolType && COMPUTER_USE_TYPES.has(toolType)) {
47
+ return {
48
+ type: "computer_20241022",
49
+ name: tool.name ?? "computer",
50
+ display_width_px: (tool.display_width_px ?? tool.display_width ?? tool.displayWidth ?? 1280),
51
+ display_height_px: (tool.display_height_px ?? tool.display_height ?? tool.displayHeight ?? 800),
52
+ display_number: (tool.display_number ?? 0),
53
+ };
54
+ }
55
+ return {
56
+ name: tool.name,
57
+ description: tool.description ?? "",
58
+ input_schema: (tool.parameters ?? tool.input_schema ?? { type: "object", properties: {} }),
59
+ };
60
+ }
61
+ function toolName(tool) {
62
+ const fn = tool.function;
63
+ const custom = tool.custom;
64
+ return tool.name ?? fn?.name ?? custom?.name;
65
+ }
66
+ function toolDescription(tool) {
67
+ const fn = tool.function;
68
+ const custom = tool.custom;
69
+ return tool.description ?? fn?.description ?? custom?.description ?? "";
70
+ }
71
+ function stripSchemaForGoogle(schema) {
72
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
73
+ return { type: "object", properties: {} };
74
+ }
75
+ const src = schema;
76
+ const out = {};
77
+ for (const key of ["type", "description", "enum", "format", "nullable"]) {
78
+ if (src[key] !== undefined)
79
+ out[key] = src[key];
80
+ }
81
+ if (Array.isArray(src.required))
82
+ out.required = src.required;
83
+ if (src.items)
84
+ out.items = stripSchemaForGoogle(src.items);
85
+ if (src.properties && typeof src.properties === "object" && !Array.isArray(src.properties)) {
86
+ out.properties = Object.fromEntries(Object.entries(src.properties).map(([k, v]) => [k, stripSchemaForGoogle(v)]));
87
+ }
88
+ if (!out.type)
89
+ out.type = "object";
90
+ if (out.type === "object" && !out.properties)
91
+ out.properties = {};
92
+ return out;
93
+ }
94
+ function toGoogleTool(tool) {
95
+ const name = toolName(tool);
96
+ if (!name)
97
+ return null;
98
+ const fn = tool.function;
99
+ const custom = tool.custom;
100
+ const parameters = tool.parameters ?? tool.input_schema ?? fn?.parameters ?? custom?.parameters ?? custom?.input_schema ?? { type: "object", properties: {} };
101
+ return {
102
+ name,
103
+ description: toolDescription(tool),
104
+ parameters: stripSchemaForGoogle(parameters),
105
+ };
106
+ }
107
+ function toOpenAIFunctionTool(tool) {
108
+ const name = toolName(tool);
109
+ if (!name)
110
+ return null;
111
+ const fn = tool.function;
112
+ const custom = tool.custom;
113
+ const parameters = tool.parameters ?? tool.input_schema ?? fn?.parameters ?? custom?.parameters ?? custom?.input_schema ?? { type: "object", properties: {} };
114
+ return {
115
+ type: "function",
116
+ function: {
117
+ name,
118
+ description: toolDescription(tool),
119
+ parameters,
120
+ },
121
+ };
122
+ }
123
+ function parseGeminiTextToolCall(text) {
124
+ const start = toolMarkupStart(text);
125
+ if (start < 0)
126
+ return null;
127
+ const tail = text.slice(start);
128
+ const browserBlock = tail.match(/<browser\s*>([\s\S]*?)<\/browser>/);
129
+ if (browserBlock) {
130
+ const fields = parseSimpleXmlFields(browserBlock[1]);
131
+ const action = String(fields.action ?? fields.command ?? "").toLowerCase();
132
+ const url = String(fields.target ?? fields.url ?? "");
133
+ if ((action === "navigate" || action === "open" || action === "visit") && url) {
134
+ return { before: text.slice(0, start).trimEnd(), name: "web_search", args: { query: url } };
135
+ }
136
+ return null;
137
+ }
138
+ const fileBlock = tail.match(/<file\s*>([\s\S]*?)<\/file>/);
139
+ if (fileBlock) {
140
+ const fields = parseSimpleXmlFields(fileBlock[1]);
141
+ const action = String(fields.action ?? "").toLowerCase();
142
+ const path = String(fields.path ?? fields.file ?? "");
143
+ const content = String(fields.content ?? fields.text ?? "");
144
+ if ((action === "create" || action === "write" || action === "save") && path) {
145
+ return {
146
+ before: text.slice(0, start).trimEnd(),
147
+ name: "exec_command",
148
+ args: { cmd: `cat > "${path}" << 'EOF'\n${content}\nEOF` },
149
+ };
150
+ }
151
+ return null;
152
+ }
153
+ const invoke = tail.match(/<invoke\s+name=["']([^"']+)["']\s*>([\s\S]*?)<\/invoke>/);
154
+ if (invoke) {
155
+ const invokeName = invoke[1];
156
+ const body = invoke[2];
157
+ const args = {};
158
+ const paramRe = /<parameter\s+name=["']([^"']+)["']\s*>([\s\S]*?)<\/parameter>/g;
159
+ for (const m of body.matchAll(paramRe))
160
+ args[m[1]] = m[2].trim();
161
+ const lc = invokeName.toLowerCase();
162
+ if (lc === "shell" || lc === "bash" || lc === "terminal") {
163
+ const cmd = args.command ?? args.cmd ?? args.shell;
164
+ if (cmd)
165
+ return { before: text.slice(0, start).trimEnd(), name: "exec_command", args: { cmd } };
166
+ }
167
+ if (lc === "browser") {
168
+ const command = String(args.command ?? "").toLowerCase();
169
+ const url = String(args.url ?? "");
170
+ if ((command === "navigate" || command === "open" || command === "visit") && url) {
171
+ return { before: text.slice(0, start).trimEnd(), name: "web_search", args: { query: url } };
172
+ }
173
+ }
174
+ if (Object.keys(args).length > 0)
175
+ return { before: text.slice(0, start).trimEnd(), name: invokeName, args };
176
+ }
177
+ const rawToolCode = tail.match(/(?:<tool_code>|tool_code)\s*\n\s*([^\n<][^\n]*)/);
178
+ if (rawToolCode) {
179
+ const cmd = rawToolCode[1].trim();
180
+ if (cmd && !cmd.startsWith("print(") && !cmd.startsWith("default_api.")) {
181
+ return { before: text.slice(0, start).trimEnd(), name: "exec_command", args: { cmd } };
182
+ }
183
+ }
184
+ const call = tail.match(/default_api\.([A-Za-z_]\w*)\(([\s\S]*?)\)/);
185
+ if (!call)
186
+ return null;
187
+ const name = call[1];
188
+ const rawArgs = call[2];
189
+ const args = {};
190
+ const kvRe = /([A-Za-z_]\w*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
191
+ for (const m of rawArgs.matchAll(kvRe)) {
192
+ const key = m[1] === "command" ? "cmd" : m[1];
193
+ args[key] = m[2] ?? m[3] ?? "";
194
+ }
195
+ if (Object.keys(args).length === 0)
196
+ return null;
197
+ return { before: text.slice(0, start).trimEnd(), name, args };
198
+ }
199
+ function parseSimpleXmlFields(body) {
200
+ const fields = {};
201
+ const fieldRe = /<([A-Za-z_]\w*)\s*>([\s\S]*?)<\/\1>/g;
202
+ for (const m of body.matchAll(fieldRe))
203
+ fields[m[1]] = m[2].trim();
204
+ return fields;
205
+ }
206
+ function toolMarkupStart(text) {
207
+ return text.search(/<tool_code>|<?\/?function_calls>?|<\/?(invoke|parameter|browser|file|action|target|path|content|section)\b|(?:^|\n)tool_code\s*\n|default_api\.[A-Za-z_]\w*\(/);
208
+ }
209
+ function stripUnparsedToolMarkup(text) {
210
+ // Remove stray closing function_calls tags (left over from already-processed tool calls)
211
+ const t = text.replace(/^\s*<\/function_calls>\s*/gm, ' ');
212
+ const start = toolMarkupStart(t);
213
+ return start >= 0 ? t.slice(0, start).trimEnd() : t.trim();
214
+ }
215
+ // Returns text that follows the tool markup block in buf (after </invoke> and any closing </function_calls>).
216
+ function extractAfterInvokeBlock(text) {
217
+ const start = toolMarkupStart(text);
218
+ if (start < 0)
219
+ return "";
220
+ const tail = text.slice(start);
221
+ const invokeClose = tail.match(/<\/invoke>/);
222
+ if (!invokeClose)
223
+ return "";
224
+ const afterInvoke = tail.slice(invokeClose.index + invokeClose[0].length);
225
+ const fcClose = afterInvoke.match(/^\s*<\/function_calls>/);
226
+ return fcClose ? afterInvoke.slice(fcClose[0].length) : afterInvoke;
227
+ }
228
+ // Parse Codex exec_command output format ??extract just the actual output text
229
+ function parseCodexToolOutput(output) {
230
+ const raw = typeof output === "string" ? output : JSON.stringify(output ?? "");
231
+ const idx = raw.indexOf("\nOutput:\n");
232
+ if (idx !== -1) {
233
+ const actual = raw.slice(idx + "\nOutput:\n".length).trim();
234
+ const exitMatch = raw.match(/Process exited with code (\d+)/);
235
+ const exitCode = exitMatch ? parseInt(exitMatch[1]) : 0;
236
+ if (!actual && exitCode === 0)
237
+ return "(�?출력 ??결과 ?�음)";
238
+ if (!actual)
239
+ return `(?�류: exit code ${exitCode} ???�당 경로???�일 ?�음)`;
240
+ return actual;
241
+ }
242
+ return raw.trim();
243
+ }
244
+ function fallbackToolResultText(output) {
245
+ const text = parseCodexToolOutput(output);
246
+ if (!text || text === "(출력 ?�음)")
247
+ return "???�행 ?�료. 출력 ?�음.";
248
+ return `???�행 결과:\n${text}`;
249
+ }
250
+ // Tools Codex actually executes when returned as function_call events.
251
+ // read_file / write_file in req.tools are system-prompt markers, NOT callable tool names.
252
+ // web_fetch is handled by the gateway itself (not Codex) ??see below.
253
+ const CODEX_EXECUTABLE = new Set(["exec_command", "shell_exec"]);
254
+ // Map Ollama tool names to real Codex-executable tools
255
+ function remapOllamaTool(name, input) {
256
+ // Gateway-handled and Codex-native web tools ??pass through unchanged
257
+ if (name === "web_fetch" || name === "web_search")
258
+ return { name, input };
259
+ if (CODEX_EXECUTABLE.has(name)) {
260
+ // Normalize field names: model may send "command" instead of "cmd"
261
+ const normalized = { ...input };
262
+ if (!normalized.cmd) {
263
+ const alt = normalized.command ?? normalized.shell ?? normalized.shell_command ?? normalized.run ?? normalized.exec;
264
+ if (alt !== undefined) {
265
+ normalized.cmd = alt;
266
+ delete normalized.command;
267
+ delete normalized.shell;
268
+ delete normalized.shell_command;
269
+ delete normalized.run;
270
+ delete normalized.exec;
271
+ }
272
+ }
273
+ return { name, input: normalized };
274
+ }
275
+ const rawPath = String(input.path ?? input.filename ?? input.file ?? input.directory ?? input.dir ?? "");
276
+ const rawQuery = String(input.query ?? input.pattern ?? input.name ?? input.filename ?? input.search ?? "");
277
+ const path = rawPath && rawPath !== "." ? rawPath : "";
278
+ const query = rawQuery && rawQuery !== "." ? rawQuery : "";
279
+ const lc = name.toLowerCase().replace(/[_\-\s]/g, "");
280
+ if (lc === "listfiles" || lc === "ls" || lc === "listdirectory" || lc === "listdir") {
281
+ return { name: "exec_command", input: { cmd: `ls ${path || "."}` } };
282
+ }
283
+ if (lc === "findfile" || lc === "searchfile" || lc === "search" || lc === "findinfiles" || lc === "findfiles") {
284
+ const target = query || path;
285
+ if (!target)
286
+ return { name: "exec_command", input: { cmd: `ls -la .` } };
287
+ return { name: "exec_command", input: { cmd: `find . -name "${target}" 2>/dev/null | head -20` } };
288
+ }
289
+ // read_file and similar ??cat
290
+ if (lc === "readfile" || lc === "read_file" || lc === "getfile" || lc === "openfile" || lc === "getfilecontent") {
291
+ const filePath = path || query;
292
+ if (!filePath)
293
+ return { name: "exec_command", input: { cmd: `ls -la .` } };
294
+ return { name: "exec_command", input: { cmd: `cat "${filePath}"` } };
295
+ }
296
+ // write_file ??echo redirect (best-effort)
297
+ if (lc === "writefile" || lc === "write_file" || lc === "createfile") {
298
+ const content = String(input.content ?? input.text ?? input.data ?? "");
299
+ const filePath = path || query;
300
+ if (!filePath)
301
+ return { name: "exec_command", input: { cmd: `echo 'no path provided'` } };
302
+ return { name: "exec_command", input: { cmd: `cat > "${filePath}" << 'EOF'\n${content}\nEOF` } };
303
+ }
304
+ if (lc === "runcommand" || lc === "execute" || lc === "run") {
305
+ const cmd = String(input.cmd ?? input.command ?? "echo 'no command'");
306
+ return { name: "exec_command", input: { cmd } };
307
+ }
308
+ // Unknown: fallback
309
+ const target = query || path;
310
+ if (!target)
311
+ return { name: "exec_command", input: { cmd: `ls -la .` } };
312
+ return { name: "exec_command", input: { cmd: `find . -name "${target}" 2>/dev/null | head -20` } };
313
+ }
314
+ export const convStore = new Map();
315
+ setInterval(() => {
316
+ const cutoff = Date.now() - 30 * 60 * 1000;
317
+ for (const [id, s] of convStore)
318
+ if (s.ts < cutoff)
319
+ convStore.delete(id);
320
+ }, 5 * 60 * 1000).unref();
321
+ // Load persisted requests from today (local time), compacting the file
322
+ function loadAndCompact() {
323
+ if (!existsSync(REQUESTS_PATH))
324
+ return [];
325
+ try {
326
+ const cutoff = todayMidnight();
327
+ const entries = readFileSync(REQUESTS_PATH, "utf-8")
328
+ .split("\n").filter(Boolean)
329
+ .map(line => { try {
330
+ return JSON.parse(line);
331
+ }
332
+ catch {
333
+ return null;
334
+ } })
335
+ .filter((e) => e !== null && e.ts >= cutoff);
336
+ // Rewrite file with only today's entries
337
+ writeFileSync(REQUESTS_PATH, entries.map(e => JSON.stringify(e)).join("\n") + (entries.length ? "\n" : ""), "utf-8");
338
+ return entries;
339
+ }
340
+ catch {
341
+ return [];
342
+ }
343
+ }
344
+ if (!existsSync(RCODEX_DIR))
345
+ mkdirSync(RCODEX_DIR, { recursive: true });
346
+ export const requestLog = loadAndCompact();
347
+ scheduleMidnightReset();
348
+ export function pushLog(entry) {
349
+ requestLog.push(entry);
350
+ const cutoff = todayMidnight();
351
+ while (requestLog.length > 0 && requestLog[0].ts < cutoff)
352
+ requestLog.shift();
353
+ return entry;
354
+ }
355
+ export function flushLog() {
356
+ try {
357
+ writeFileSync(REQUESTS_PATH, requestLog.map(e => JSON.stringify(e)).join("\n") + (requestLog.length ? "\n" : ""), "utf-8");
358
+ }
359
+ catch { /* ignore */ }
360
+ }
361
+ // ?�?� Input parsing ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
362
+ // Normalize Responses API content types (input_text, output_text, input_image)
363
+ // to Anthropic-compatible types (text, image).
364
+ function normalizeContentType(type) {
365
+ if (!type)
366
+ return "text";
367
+ if (type === "input_text" || type === "output_text")
368
+ return "text";
369
+ if (type === "input_image")
370
+ return "image";
371
+ return type;
372
+ }
373
+ function parseInput(input) {
374
+ if (!input)
375
+ return [];
376
+ if (typeof input === "string")
377
+ return [{ role: "user", content: input }];
378
+ const items = input;
379
+ return items
380
+ .filter(item => item.role && item.content != null)
381
+ .map(item => ({
382
+ role: item.role,
383
+ content: typeof item.content === "string"
384
+ ? item.content
385
+ : item.content.map(c => ({ type: normalizeContentType(c.type), text: c.text ?? "" })),
386
+ }));
387
+ }
388
+ function toOpenAIChatMessages(input, fallback) {
389
+ if (!Array.isArray(input)) {
390
+ return fallback.map(m => ({
391
+ role: m.role === "developer" ? "system" : m.role,
392
+ content: typeof m.content === "string" ? m.content : m.content.map(c => c.text ?? "").join(""),
393
+ }));
394
+ }
395
+ const inputArr = input;
396
+ const hasFunctionItems = inputArr.some(i => i.type === "function_call" || i.type === "function_call_output");
397
+ if (!hasFunctionItems) {
398
+ return fallback.map(m => ({
399
+ role: m.role === "developer" ? "system" : m.role,
400
+ content: typeof m.content === "string" ? m.content : m.content.map(c => c.text ?? "").join(""),
401
+ }));
402
+ }
403
+ const msgs = [];
404
+ let pendingToolCalls = [];
405
+ let pendingText = "";
406
+ const flushAssistant = () => {
407
+ if (pendingToolCalls.length > 0 || pendingText) {
408
+ msgs.push({
409
+ role: "assistant",
410
+ content: pendingText || null,
411
+ ...(pendingToolCalls.length > 0 ? { tool_calls: pendingToolCalls } : {}),
412
+ });
413
+ pendingToolCalls = [];
414
+ pendingText = "";
415
+ }
416
+ };
417
+ for (const item of inputArr) {
418
+ const type = item.type;
419
+ if (type === "message") {
420
+ flushAssistant();
421
+ const content = typeof item.content === "string" ? item.content
422
+ : (item.content ?? []).map(c => c.text ?? "").join("");
423
+ const role = (item.role || "user") === "developer" ? "system" : (item.role || "user");
424
+ if (role === "assistant")
425
+ pendingText += content;
426
+ else
427
+ msgs.push({ role, content });
428
+ }
429
+ else if (type === "function_call") {
430
+ pendingToolCalls.push({
431
+ id: item.call_id ?? item.id ?? `call_${Date.now()}`,
432
+ type: "function",
433
+ function: {
434
+ name: item.name,
435
+ arguments: typeof item.arguments === "string" ? item.arguments : JSON.stringify(item.arguments ?? {}),
436
+ },
437
+ });
438
+ }
439
+ else if (type === "function_call_output") {
440
+ flushAssistant();
441
+ msgs.push({ role: "tool", tool_call_id: item.call_id, content: parseCodexToolOutput(item.output) });
442
+ }
443
+ }
444
+ flushAssistant();
445
+ return msgs;
446
+ }
447
+ // ?�?� Response format converters ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
448
+ function chatToResponses(data, model) {
449
+ const text = data.choices?.[0]?.message?.content ?? "";
450
+ return {
451
+ id: data.id ?? `resp_${Date.now()}`,
452
+ object: "response",
453
+ model,
454
+ output: [{ type: "message", role: "assistant", content: [{ type: "output_text", text }] }],
455
+ usage: data.usage ? { input_tokens: data.usage.prompt_tokens, output_tokens: data.usage.completion_tokens, total_tokens: data.usage.total_tokens } : undefined,
456
+ };
457
+ }
458
+ function anthropicToResponses(data, model) {
459
+ const text = data.content?.find(b => b.type === "text")?.text ?? "";
460
+ return {
461
+ id: data.id ?? `resp_${Date.now()}`,
462
+ object: "response",
463
+ model,
464
+ output: [{ type: "message", role: "assistant", content: [{ type: "output_text", text }] }],
465
+ usage: data.usage ? { input_tokens: data.usage.input_tokens, output_tokens: data.usage.output_tokens, total_tokens: data.usage.input_tokens + data.usage.output_tokens } : undefined,
466
+ };
467
+ }
468
+ function googleToResponses(data, model) {
469
+ const text = data.candidates?.[0]?.content?.parts?.[0]?.text ?? "";
470
+ const inputTokens = data.usageMetadata?.promptTokenCount;
471
+ const outputTokens = data.usageMetadata?.candidatesTokenCount;
472
+ const totalTokens = data.usageMetadata?.totalTokenCount ??
473
+ (inputTokens !== undefined && outputTokens !== undefined ? inputTokens + outputTokens : undefined);
474
+ return {
475
+ id: `resp_${Date.now()}`,
476
+ object: "response",
477
+ model,
478
+ output: [{ type: "message", role: "assistant", content: [{ type: "output_text", text }] }],
479
+ usage: inputTokens !== undefined || outputTokens !== undefined || totalTokens !== undefined
480
+ ? {
481
+ input_tokens: inputTokens ?? 0,
482
+ output_tokens: outputTokens ?? 0,
483
+ total_tokens: totalTokens ?? (inputTokens ?? 0) + (outputTokens ?? 0),
484
+ }
485
+ : undefined,
486
+ };
487
+ }
488
+ // ?�?� Codex request builder ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
489
+ function buildCodexBody(req, model) {
490
+ const body = {
491
+ model,
492
+ input: req.input,
493
+ stream: true,
494
+ store: req.store ?? false,
495
+ reasoning: req.reasoning ?? { effort: "low", summary: "auto" },
496
+ include: req.include ?? ["reasoning.encrypted_content"],
497
+ };
498
+ if (req.instructions)
499
+ body.instructions = req.instructions;
500
+ if (req.temperature !== undefined)
501
+ body.temperature = req.temperature;
502
+ if (req.max_output_tokens !== undefined)
503
+ body.max_output_tokens = req.max_output_tokens;
504
+ if (req.previous_response_id)
505
+ body.previous_response_id = req.previous_response_id;
506
+ if (req.tools)
507
+ body.tools = req.tools;
508
+ if (req.tool_choice)
509
+ body.tool_choice = req.tool_choice;
510
+ return body;
511
+ }
512
+ // ?�?� Codex OAuth transparent passthrough ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
513
+ // Forwards the FULL original request body (tools, previous_response_id, etc.)
514
+ // so Codex-native models (gpt-5.5 etc.) can execute shell/file tools normally.
515
+ export async function tryCodexPassthrough(rawBody, config, signal) {
516
+ const model = rawBody.model;
517
+ if (!model)
518
+ return null;
519
+ const candidates = resolveAccounts(model, config);
520
+ const first = candidates[0];
521
+ const hasPrevId = !!rawBody.previous_response_id;
522
+ // If previous_response_id is in our convStore, we handled that turn ??don't
523
+ // passthrough, the follow-up also belongs to our proxy (e.g. Claude tool result)
524
+ const prevIdIsOurs = hasPrevId && convStore.has(rawBody.previous_response_id);
525
+ let codexCandidate;
526
+ if (!prevIdIsOurs) {
527
+ if (first?.account.method === "oauth-official" && first.account.provider === "openai") {
528
+ // Codex OAuth is slot 1 ??always passthrough
529
+ codexCandidate = first;
530
+ }
531
+ else if (hasPrevId) {
532
+ // previous_response_id not from our proxy ??may be a Codex-native multi-turn ID
533
+ // Look for any Codex OAuth as fallback (only for hasPrevId, NOT hasTools alone)
534
+ codexCandidate = candidates.find(c => c.account.method === "oauth-official" && c.account.provider === "openai");
535
+ }
536
+ // NOTE: hasTools alone no longer triggers passthrough ??Claude/Gemini/Ollama handle tools too
537
+ }
538
+ if (!codexCandidate)
539
+ return null;
540
+ const account = await ensureFreshToken(codexCandidate.account);
541
+ if (!account.oauthToken)
542
+ return null;
543
+ const body = { ...rawBody, model: codexCandidate.model, stream: true };
544
+ try {
545
+ const res = await callCodexAPI(account.oauthToken, body, signal);
546
+ if (!res.ok) {
547
+ gerr(`[gateway] Codex passthrough error: ${res.status}`);
548
+ return null;
549
+ }
550
+ glog(`[gateway] ??Codex passthrough: ${codexCandidate.model}`);
551
+ return { res, account, model: codexCandidate.model };
552
+ }
553
+ catch (err) {
554
+ gerr(`[gateway] Codex passthrough failed: ${err instanceof Error ? err.message : String(err)}`);
555
+ return null;
556
+ }
557
+ }
558
+ // ?�?� SSE stream reader ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
559
+ async function* readSSE(res, extract) {
560
+ if (!res.body)
561
+ return;
562
+ const reader = res.body.getReader();
563
+ const decoder = new TextDecoder();
564
+ let buf = "";
565
+ try {
566
+ while (true) {
567
+ const { done, value } = await reader.read();
568
+ if (done)
569
+ break;
570
+ buf += decoder.decode(value, { stream: true });
571
+ const lines = buf.split("\n");
572
+ buf = lines.pop() ?? "";
573
+ for (const line of lines) {
574
+ if (!line.startsWith("data: "))
575
+ continue;
576
+ const raw = line.slice(6).trim();
577
+ if (raw === "[DONE]")
578
+ return;
579
+ try {
580
+ const delta = extract(JSON.parse(raw));
581
+ if (delta)
582
+ yield delta;
583
+ }
584
+ catch { /* skip malformed */ }
585
+ }
586
+ }
587
+ }
588
+ finally {
589
+ reader.releaseLock();
590
+ }
591
+ }
592
+ // ?�?� Codex (Responses API) SSE reader ??passes through reasoning + text ?�?�?�?�?�?�?�?�
593
+ async function* readCodexSSEWithUsage(res, usage) {
594
+ if (!res.body)
595
+ return;
596
+ const reader = res.body.getReader();
597
+ const decoder = new TextDecoder();
598
+ let buf = "";
599
+ try {
600
+ while (true) {
601
+ const { done, value } = await reader.read();
602
+ if (done)
603
+ break;
604
+ buf += decoder.decode(value, { stream: true });
605
+ const lines = buf.split("\n");
606
+ buf = lines.pop() ?? "";
607
+ for (const line of lines) {
608
+ if (!line.startsWith("data: "))
609
+ continue;
610
+ const raw = line.slice(6).trim();
611
+ if (raw === "[DONE]")
612
+ return;
613
+ try {
614
+ const evt = JSON.parse(raw);
615
+ if (evt.type === "response.reasoning.delta") {
616
+ const d = evt.delta;
617
+ if (d?.text)
618
+ yield { type: 'reasoning', content: d.text };
619
+ }
620
+ else if (evt.type === "response.output_text.delta") {
621
+ const delta = evt.delta;
622
+ if (delta)
623
+ yield { type: 'text', content: delta };
624
+ }
625
+ else if (evt.type === "response.completed") {
626
+ const resp = evt.response;
627
+ if (resp?.usage?.input_tokens)
628
+ usage.inputTokens = resp.usage.input_tokens;
629
+ if (resp?.usage?.output_tokens)
630
+ usage.outputTokens = resp.usage.output_tokens;
631
+ }
632
+ }
633
+ catch { /* skip malformed */ }
634
+ }
635
+ }
636
+ }
637
+ finally {
638
+ reader.releaseLock();
639
+ }
640
+ }
641
+ // ?�?� <think> tag parser (stateful, handles partial tags across chunks) ?�?�?�?�?�?�?�?�?�?�
642
+ function* parseThinkChunks(state, incoming, final = false) {
643
+ state.buf += incoming;
644
+ const OPEN = "<think>", CLOSE = "</think>";
645
+ while (true) {
646
+ if (!state.inThink) {
647
+ const s = state.buf.indexOf(OPEN);
648
+ if (s === -1) {
649
+ const safe = final ? state.buf.length : Math.max(0, state.buf.length - OPEN.length + 1);
650
+ if (safe > 0) {
651
+ yield { type: 'text', content: state.buf.slice(0, safe) };
652
+ state.buf = state.buf.slice(safe);
653
+ }
654
+ break;
655
+ }
656
+ if (s > 0)
657
+ yield { type: 'text', content: state.buf.slice(0, s) };
658
+ state.buf = state.buf.slice(s + OPEN.length);
659
+ state.inThink = true;
660
+ }
661
+ else {
662
+ const e = state.buf.indexOf(CLOSE);
663
+ if (e === -1) {
664
+ const safe = final ? state.buf.length : Math.max(0, state.buf.length - CLOSE.length + 1);
665
+ if (safe > 0) {
666
+ yield { type: 'reasoning', content: state.buf.slice(0, safe) };
667
+ state.buf = state.buf.slice(safe);
668
+ }
669
+ break;
670
+ }
671
+ if (e > 0)
672
+ yield { type: 'reasoning', content: state.buf.slice(0, e) };
673
+ state.buf = state.buf.slice(e + CLOSE.length);
674
+ state.inThink = false;
675
+ }
676
+ }
677
+ }
678
+ // ?�?� OpenAI-format SSE reader ??captures usage, <think> tags, and tool_calls ?�?�?�?�
679
+ async function* readOpenAISSEWithUsage(res, usage, capturedTools, capturedText, disableTextTools) {
680
+ if (!res.body)
681
+ return;
682
+ const reader = res.body.getReader();
683
+ const decoder = new TextDecoder();
684
+ let buf = "";
685
+ const thinkState = { buf: '', inThink: false };
686
+ let textBufForToolMarkup = "";
687
+ let channelBuf = "";
688
+ let suppressUntilChannel = false;
689
+ // OpenAI tool calls stream as index-keyed deltas
690
+ const toolCalls = new Map();
691
+ function toolArgsDelta(value) {
692
+ if (typeof value === "string")
693
+ return value;
694
+ if (value && typeof value === "object")
695
+ return JSON.stringify(value);
696
+ return null;
697
+ }
698
+ function takeChannelMarker(input) {
699
+ const match = input.match(/<\|?channel\|?>/);
700
+ return match?.index === undefined ? null : { index: match.index, length: match[0].length };
701
+ }
702
+ // Splits channel-markup buffer into thought segments (??reasoning) and visible segments (??text/tool).
703
+ // Gemma models emit: thought\n[thinking]\n<channel|>\n[visible response]
704
+ function splitChannelMarkup(text, final) {
705
+ channelBuf += text;
706
+ const segs = [];
707
+ while (channelBuf) {
708
+ if (suppressUntilChannel) {
709
+ const marker = takeChannelMarker(channelBuf);
710
+ if (!marker) {
711
+ if (final) {
712
+ if (channelBuf.trim())
713
+ segs.push({ kind: 'thought', content: channelBuf });
714
+ channelBuf = "";
715
+ suppressUntilChannel = false;
716
+ }
717
+ return segs;
718
+ }
719
+ const thought = channelBuf.slice(0, marker.index);
720
+ if (thought.trim())
721
+ segs.push({ kind: 'thought', content: thought });
722
+ channelBuf = channelBuf.slice(marker.index + marker.length);
723
+ suppressUntilChannel = false;
724
+ continue;
725
+ }
726
+ if (/^thought\b\s*/.test(channelBuf)) {
727
+ channelBuf = channelBuf.replace(/^thought\b\s*/, "");
728
+ suppressUntilChannel = true;
729
+ continue;
730
+ }
731
+ const marker = takeChannelMarker(channelBuf);
732
+ if (marker) {
733
+ const before = channelBuf.slice(0, marker.index);
734
+ if (before)
735
+ segs.push({ kind: 'visible', content: before });
736
+ channelBuf = channelBuf.slice(marker.index + marker.length);
737
+ continue;
738
+ }
739
+ const keep = final ? 0 : 32;
740
+ if (channelBuf.length <= keep)
741
+ return segs;
742
+ segs.push({ kind: 'visible', content: channelBuf.slice(0, channelBuf.length - keep) });
743
+ channelBuf = channelBuf.slice(-keep);
744
+ return segs;
745
+ }
746
+ return segs;
747
+ }
748
+ function* flushToolCalls() {
749
+ for (const [idx, call] of toolCalls) {
750
+ let rawInput = {};
751
+ try {
752
+ rawInput = JSON.parse(call.args || "{}");
753
+ }
754
+ catch { /* ignore */ }
755
+ const { name: finalName, input: finalInput } = remapOllamaTool(call.name, rawInput);
756
+ if (finalName !== call.name)
757
+ glog(`[ollama] remap tool: ${call.name} ??${finalName}(${JSON.stringify(finalInput)})`);
758
+ else
759
+ glog(`[ollama] tool call: ${finalName}(${call.args})`);
760
+ const finalArgs = JSON.stringify(finalInput);
761
+ yield { type: 'tool_call_start', id: call.id, name: finalName, index: idx };
762
+ yield { type: 'tool_call_delta', id: call.id, delta: finalArgs, index: idx };
763
+ yield { type: 'tool_call_end', id: call.id, name: finalName, arguments: finalArgs, index: idx };
764
+ if (capturedTools)
765
+ capturedTools.push({ id: call.id, name: finalName, input: finalInput, rawArgs: finalArgs });
766
+ }
767
+ toolCalls.clear();
768
+ }
769
+ function* emitTextToolCall(buf) {
770
+ const parsedTool = parseGeminiTextToolCall(buf);
771
+ if (!parsedTool)
772
+ return;
773
+ if (parsedTool.before) {
774
+ if (capturedText)
775
+ capturedText.value += parsedTool.before;
776
+ yield* parseThinkChunks(thinkState, parsedTool.before);
777
+ }
778
+ const { name: finalName, input: finalInput } = remapOllamaTool(parsedTool.name, parsedTool.args);
779
+ const id = `call_${Date.now()}_${toolCalls.size}`;
780
+ const argText = JSON.stringify(finalInput);
781
+ yield { type: 'tool_call_start', id, name: finalName, index: 0 };
782
+ yield { type: 'tool_call_delta', id, delta: argText, index: 0 };
783
+ yield { type: 'tool_call_end', id, name: finalName, arguments: argText, index: 0 };
784
+ if (capturedTools)
785
+ capturedTools.push({ id, name: finalName, input: finalInput, rawArgs: argText });
786
+ // Preserve text after </invoke></function_calls> instead of discarding it
787
+ textBufForToolMarkup = extractAfterInvokeBlock(buf);
788
+ }
789
+ function* handleTextDelta(text, final = false) {
790
+ const segs = splitChannelMarkup(text, final);
791
+ for (const seg of segs) {
792
+ if (seg.kind === 'thought') {
793
+ // Gemma thought channel ??show as reasoning (gray in Codex UI)
794
+ yield { type: 'reasoning', content: seg.content };
795
+ continue;
796
+ }
797
+ if (disableTextTools) {
798
+ // Summarize turn: emit text directly, no tool markup parsing
799
+ if (capturedText)
800
+ capturedText.value += seg.content;
801
+ yield* parseThinkChunks(thinkState, seg.content);
802
+ continue;
803
+ }
804
+ // Visible segment ??run through tool-markup detection and think-tag parser
805
+ textBufForToolMarkup += seg.content;
806
+ const parsedTool = parseGeminiTextToolCall(textBufForToolMarkup);
807
+ if (parsedTool) {
808
+ yield* emitTextToolCall(textBufForToolMarkup);
809
+ continue;
810
+ }
811
+ const markerStart = toolMarkupStart(textBufForToolMarkup);
812
+ if (markerStart >= 0)
813
+ continue; // hold back ??more chunks may complete the markup
814
+ const safe = Math.max(0, textBufForToolMarkup.length - 32);
815
+ if (safe > 0) {
816
+ const out = textBufForToolMarkup.slice(0, safe);
817
+ if (capturedText)
818
+ capturedText.value += out;
819
+ yield* parseThinkChunks(thinkState, out);
820
+ textBufForToolMarkup = textBufForToolMarkup.slice(safe);
821
+ }
822
+ }
823
+ // Final flush: try tool parse first (catches multi-chunk markup), then emit as text
824
+ if (final && textBufForToolMarkup) {
825
+ if (disableTextTools) {
826
+ if (capturedText)
827
+ capturedText.value += textBufForToolMarkup;
828
+ yield* parseThinkChunks(thinkState, textBufForToolMarkup);
829
+ textBufForToolMarkup = "";
830
+ }
831
+ else {
832
+ // Loop: one response may contain multiple sequential tool calls (e.g. Claude XML format)
833
+ let guard = 0;
834
+ while (textBufForToolMarkup && guard++ < 20) {
835
+ const parsedTool = parseGeminiTextToolCall(textBufForToolMarkup);
836
+ if (parsedTool) {
837
+ yield* emitTextToolCall(textBufForToolMarkup); // updates textBufForToolMarkup to after
838
+ }
839
+ else {
840
+ const cleaned = stripUnparsedToolMarkup(textBufForToolMarkup);
841
+ if (cleaned) {
842
+ if (capturedText)
843
+ capturedText.value += cleaned;
844
+ yield* parseThinkChunks(thinkState, cleaned);
845
+ }
846
+ textBufForToolMarkup = "";
847
+ }
848
+ }
849
+ }
850
+ }
851
+ }
852
+ try {
853
+ while (true) {
854
+ const { done, value } = await reader.read();
855
+ if (done)
856
+ break;
857
+ buf += decoder.decode(value, { stream: true });
858
+ const lines = buf.split("\n");
859
+ buf = lines.pop() ?? "";
860
+ for (const line of lines) {
861
+ if (!line.startsWith("data: "))
862
+ continue;
863
+ const raw = line.slice(6).trim();
864
+ if (raw === "[DONE]") {
865
+ yield* handleTextDelta('', true);
866
+ yield* parseThinkChunks(thinkState, '', true);
867
+ yield* flushToolCalls();
868
+ return;
869
+ }
870
+ try {
871
+ const evt = JSON.parse(raw);
872
+ const choice = evt.choices?.[0];
873
+ const delta = choice?.delta;
874
+ if (delta?.content) {
875
+ yield* handleTextDelta(delta.content);
876
+ }
877
+ if (delta?.tool_calls) {
878
+ for (const tc of delta.tool_calls) {
879
+ let existing = toolCalls.get(tc.index);
880
+ const argsDelta = toolArgsDelta(tc.function?.arguments);
881
+ if (!existing && tc.function?.name !== undefined) {
882
+ const id = tc.id ?? `call_${Date.now()}_${tc.index}`;
883
+ existing = { id, name: tc.function.name, args: "" };
884
+ toolCalls.set(tc.index, existing);
885
+ // Don't emit tool_call_start yet ??normalize name+args in flushToolCalls
886
+ }
887
+ if (existing && argsDelta) {
888
+ existing.args += argsDelta;
889
+ // Don't emit tool_call_delta yet ??emit normalized version in flushToolCalls
890
+ }
891
+ }
892
+ }
893
+ // On summarize turns (disableTextTools), skip native tool_calls too ??some models
894
+ // (qwen3) emit tool_calls even when tools are absent; ignoring prevents infinite loops.
895
+ if (choice?.finish_reason === "tool_calls" && !disableTextTools)
896
+ yield* flushToolCalls();
897
+ const u = evt.usage;
898
+ if (u?.prompt_tokens)
899
+ usage.inputTokens = u.prompt_tokens;
900
+ if (u?.completion_tokens)
901
+ usage.outputTokens = u.completion_tokens;
902
+ }
903
+ catch { /* skip malformed */ }
904
+ }
905
+ }
906
+ yield* handleTextDelta('', true);
907
+ yield* parseThinkChunks(thinkState, '', true);
908
+ if (!disableTextTools)
909
+ yield* flushToolCalls();
910
+ }
911
+ finally {
912
+ reader.releaseLock();
913
+ }
914
+ }
915
+ // ?�?� Token refresh ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
916
+ export async function ensureFreshToken(account) {
917
+ if (account.method !== "oauth-official" || !account.oauthRefresh || !account.oauthExpiry)
918
+ return account;
919
+ if (Date.now() < account.oauthExpiry - 5 * 60 * 1000)
920
+ return account;
921
+ try {
922
+ const tokens = account.provider === "anthropic"
923
+ ? await refreshClaudeAccessToken(account.oauthRefresh)
924
+ : account.provider === "antigravity"
925
+ ? await refreshAntigravityAccessToken(account.oauthRefresh)
926
+ : await refreshOpenAIAccessToken(account.oauthRefresh);
927
+ const expiry = Date.now() + tokens.expiresIn * 1000;
928
+ updateAccountOAuth(account.id, tokens.accessToken, tokens.refreshToken, expiry);
929
+ glog(`[gateway] ??token refreshed: ${account.provider}(${account.label})`);
930
+ return { ...account, oauthToken: tokens.accessToken, oauthRefresh: tokens.refreshToken, oauthExpiry: expiry };
931
+ }
932
+ catch (err) {
933
+ gerr(`[gateway] token refresh failed: ${account.provider} ??${err instanceof Error ? err.message : String(err)}`);
934
+ return account;
935
+ }
936
+ }
937
+ // ?�?� Anthropic SSE reader that also captures usage and tool_use blocks ?�?�?�?�?�?�?�?�?�?�
938
+ async function* readAnthropicSSEWithUsage(res, usage, capturedTools, capturedText) {
939
+ if (!res.body)
940
+ return;
941
+ const reader = res.body.getReader();
942
+ const decoder = new TextDecoder();
943
+ let buf = "";
944
+ // Track tool_use blocks being streamed, keyed by content block index
945
+ const toolBlocks = new Map();
946
+ try {
947
+ while (true) {
948
+ const { done, value } = await reader.read();
949
+ if (done)
950
+ break;
951
+ buf += decoder.decode(value, { stream: true });
952
+ const lines = buf.split("\n");
953
+ buf = lines.pop() ?? "";
954
+ for (const line of lines) {
955
+ if (!line.startsWith("data: "))
956
+ continue;
957
+ const raw = line.slice(6).trim();
958
+ if (raw === "[DONE]")
959
+ return;
960
+ try {
961
+ const evt = JSON.parse(raw);
962
+ if (evt.type === "message_start") {
963
+ const u = evt.message?.usage;
964
+ if (u?.input_tokens)
965
+ usage.inputTokens = u.input_tokens;
966
+ }
967
+ else if (evt.type === "message_delta") {
968
+ const u = evt.usage;
969
+ if (u?.output_tokens)
970
+ usage.outputTokens = u.output_tokens;
971
+ }
972
+ else if (evt.type === "content_block_start") {
973
+ const idx = evt.index;
974
+ const cb = evt.content_block;
975
+ if (cb?.type === "tool_use" && cb.id && cb.name) {
976
+ toolBlocks.set(idx, { id: cb.id, name: cb.name, args: "" });
977
+ yield { type: 'tool_call_start', id: cb.id, name: cb.name, index: idx };
978
+ }
979
+ }
980
+ else if (evt.type === "content_block_delta") {
981
+ const idx = evt.index;
982
+ const d = evt.delta;
983
+ if (d?.type === "thinking_delta" && d.thinking) {
984
+ yield { type: 'reasoning', content: d.thinking };
985
+ }
986
+ else if (d?.type === "text_delta" && d.text) {
987
+ if (capturedText)
988
+ capturedText.value += d.text;
989
+ yield { type: 'text', content: d.text };
990
+ }
991
+ else if (d?.type === "input_json_delta" && d.partial_json) {
992
+ const block = toolBlocks.get(idx);
993
+ if (block) {
994
+ block.args += d.partial_json;
995
+ yield { type: 'tool_call_delta', id: block.id, delta: d.partial_json, index: idx };
996
+ }
997
+ }
998
+ }
999
+ else if (evt.type === "content_block_stop") {
1000
+ const idx = evt.index;
1001
+ const block = toolBlocks.get(idx);
1002
+ if (block) {
1003
+ yield { type: 'tool_call_end', id: block.id, name: block.name, arguments: block.args, index: idx };
1004
+ if (capturedTools) {
1005
+ try {
1006
+ capturedTools.push({ id: block.id, name: block.name, input: JSON.parse(block.args || "{}") });
1007
+ }
1008
+ catch {
1009
+ capturedTools.push({ id: block.id, name: block.name, input: {} });
1010
+ }
1011
+ }
1012
+ toolBlocks.delete(idx);
1013
+ }
1014
+ }
1015
+ }
1016
+ catch { /* skip malformed */ }
1017
+ }
1018
+ }
1019
+ }
1020
+ finally {
1021
+ reader.releaseLock();
1022
+ }
1023
+ }
1024
+ // ?�?� Google / Antigravity SSE reader ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
1025
+ async function* readGoogleSSEWithUsage(res, usage, capturedTools, capturedText) {
1026
+ if (!res.body)
1027
+ return;
1028
+ const reader = res.body.getReader();
1029
+ const decoder = new TextDecoder();
1030
+ let buf = "";
1031
+ let toolIdx = 0;
1032
+ // thoughtSignature arrives on the thought part, not the functionCall part ??track across SSE events
1033
+ let pendingThoughtSig;
1034
+ try {
1035
+ while (true) {
1036
+ const { done, value } = await reader.read();
1037
+ if (done)
1038
+ break;
1039
+ buf += decoder.decode(value, { stream: true });
1040
+ const lines = buf.split("\n");
1041
+ buf = lines.pop() ?? "";
1042
+ for (const line of lines) {
1043
+ if (!line.startsWith("data: "))
1044
+ continue;
1045
+ const raw = line.slice(6).trim();
1046
+ if (raw === "[DONE]")
1047
+ return;
1048
+ try {
1049
+ const parsed = JSON.parse(raw);
1050
+ // Antigravity wraps payload in { response: { candidates, usageMetadata, ... } }
1051
+ const evt = parsed.response ?? parsed;
1052
+ if (evt.usageMetadata?.promptTokenCount)
1053
+ usage.inputTokens = evt.usageMetadata.promptTokenCount;
1054
+ if (evt.usageMetadata?.candidatesTokenCount)
1055
+ usage.outputTokens = evt.usageMetadata.candidatesTokenCount;
1056
+ for (const part of evt.candidates?.[0]?.content?.parts ?? []) {
1057
+ if (part.thought) {
1058
+ // capture thoughtSignature from thought part; attach to the next functionCall
1059
+ if (part.thoughtSignature)
1060
+ pendingThoughtSig = part.thoughtSignature;
1061
+ continue;
1062
+ }
1063
+ if (part.text) {
1064
+ if (capturedText)
1065
+ capturedText.value += part.text;
1066
+ yield { type: 'text', content: part.text };
1067
+ }
1068
+ if (part.functionCall?.name) {
1069
+ const id = `call_g_${Date.now()}_${toolIdx}`;
1070
+ const args = part.functionCall.args ?? {};
1071
+ const argText = JSON.stringify(args);
1072
+ const thoughtSig = pendingThoughtSig ?? part.thoughtSignature;
1073
+ pendingThoughtSig = undefined;
1074
+ if (capturedTools)
1075
+ capturedTools.push({ id, name: part.functionCall.name, input: args, thoughtSignature: thoughtSig });
1076
+ yield { type: 'tool_call_start', id, name: part.functionCall.name, index: toolIdx };
1077
+ yield { type: 'tool_call_delta', id, delta: argText, index: toolIdx };
1078
+ yield { type: 'tool_call_end', id, name: part.functionCall.name, arguments: argText, index: toolIdx };
1079
+ toolIdx++;
1080
+ }
1081
+ }
1082
+ }
1083
+ catch { /* skip malformed */ }
1084
+ }
1085
+ }
1086
+ }
1087
+ finally {
1088
+ reader.releaseLock();
1089
+ }
1090
+ }
1091
+ // ?�?� SSE extractors ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
1092
+ const chatExtract = (evt) => {
1093
+ const choices = evt.choices;
1094
+ return choices?.[0]?.delta?.content ?? null;
1095
+ };
1096
+ const codexExtract = (evt) => evt.type === "response.output_text.delta"
1097
+ ? (evt.delta ?? null) : null;
1098
+ const anthropicExtract = (evt) => evt.type === "content_block_delta" &&
1099
+ evt.delta?.type === "text_delta"
1100
+ ? evt.delta.text ?? null : null;
1101
+ // ?�?� Account resolution: sorted by priority (slot order) ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
1102
+ function resolveAccounts(requestedModel, config) {
1103
+ const safeDefault = {
1104
+ anthropic: "claude-opus-4-7-20250514",
1105
+ openai: "gpt-4o",
1106
+ google: "gemini-2.0-flash",
1107
+ ollama: requestedModel,
1108
+ antigravity: "ag/gemini-3.1-pro-high",
1109
+ copilot: "gpt-4o",
1110
+ };
1111
+ const providerFromModel = isAnthropicModel(requestedModel) ? "anthropic" :
1112
+ isOpenAIModel(requestedModel) ? "openai" :
1113
+ isGoogleModel(requestedModel) ? "google" :
1114
+ isOllamaModel(requestedModel) ? "ollama" :
1115
+ isAntigravityModel(requestedModel) ? "antigravity" : null;
1116
+ const candidates = [];
1117
+ for (const account of config.accounts) {
1118
+ if (account.activeModels?.length) {
1119
+ // New multi-slot routing
1120
+ for (const slot of account.activeModels) {
1121
+ const model = slot.model || safeDefault[account.provider] || requestedModel;
1122
+ candidates.push({ account, model, order: slot.order });
1123
+ }
1124
+ }
1125
+ else if (account.connectedToOut) {
1126
+ // Legacy single-connection fallback
1127
+ const model = account.selectedModel ||
1128
+ (providerFromModel === account.provider ? requestedModel : safeDefault[account.provider] || requestedModel);
1129
+ candidates.push({ account, model, order: account.connectedOrder ?? 999 });
1130
+ }
1131
+ }
1132
+ return candidates
1133
+ .sort((a, b) => a.order - b.order)
1134
+ .map(({ account, model }) => ({ account, model }));
1135
+ }
1136
+ // ?�?� Strip Codex tool instructions for non-Codex providers ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
1137
+ // Codex always includes tool definitions (exec_command, shell_exec, etc.) in the
1138
+ // system prompt. Non-Codex providers (Claude, Gemini, Ollama) would try to output
1139
+ // tool calls in their own format (e.g., Claude's XML <function_calls> tags),
1140
+ // which appears as raw text in Codex UI. Strip them so the model responds as
1141
+ // a plain assistant without tool use.
1142
+ const CODEX_TOOL_MARKERS = ['exec_command', 'shell_exec', 'read_file', 'write_file', 'workspace-write', 'function_calls', 'computer_call'];
1143
+ function cleanInstructions(instructions) {
1144
+ if (!instructions)
1145
+ return undefined;
1146
+ if (CODEX_TOOL_MARKERS.some(m => instructions.includes(m)))
1147
+ return undefined;
1148
+ return instructions;
1149
+ }
1150
+ function buildOpenAIToolsForProvider(req, skipTypes = new Set()) {
1151
+ if (!Array.isArray(req.tools) || req.tools.length === 0)
1152
+ return undefined;
1153
+ const tools = req.tools
1154
+ .filter(t => {
1155
+ const type = t.type;
1156
+ if (type && skipTypes.has(type))
1157
+ return false;
1158
+ return toolName(t) != null;
1159
+ })
1160
+ .map(toOpenAIFunctionTool)
1161
+ .filter((t) => t !== null);
1162
+ return tools.length > 0 ? tools : undefined;
1163
+ }
1164
+ function copilotInstructions(req, tools) {
1165
+ if (!tools || tools.length === 0)
1166
+ return cleanInstructions(req.instructions);
1167
+ return [
1168
+ "You are an AI assistant. Respond in the same language as the user.",
1169
+ "Use the provided function tools for file, command, filesystem, browser, or web tasks.",
1170
+ "Never claim that a file was created, modified, read, or that a website was visited unless a tool result confirms it.",
1171
+ "Do not output XML, <function_calls>, <browser>, <file>, <tool_code>, or pseudo-tool tags as text.",
1172
+ ].join("\n");
1173
+ }
1174
+ // ?�?� Single provider call (non-streaming) ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
1175
+ async function callSingleProvider(rawAccount, model, req, config, signal) {
1176
+ let account = rawAccount;
1177
+ const messages = parseInput(req.input);
1178
+ const { instructions } = req;
1179
+ if (account.provider === "anthropic") {
1180
+ account = await ensureFreshToken(account);
1181
+ const auth = accountToProviderAuth(account);
1182
+ const res = await callAnthropic(auth, model, messages, cleanInstructions(instructions), false, signal);
1183
+ if (!res.ok)
1184
+ throw new Error(`Anthropic error ${res.status}: ${await res.text()}`);
1185
+ return anthropicToResponses(await res.json(), model);
1186
+ }
1187
+ if (account.provider === "google") {
1188
+ const auth = accountToProviderAuth(account);
1189
+ const res = await callGoogle(auth, model, messages, cleanInstructions(instructions), false, signal);
1190
+ if (!res.ok)
1191
+ throw new Error(`Google error ${res.status}: ${await res.text()}`);
1192
+ return googleToResponses(await res.json(), model);
1193
+ }
1194
+ if (account.provider === "ollama") {
1195
+ const res = await callOllama(config.ollamaBaseUrl, model, messages, cleanInstructions(instructions), false, signal);
1196
+ if (!res.ok)
1197
+ throw new Error(`Ollama error ${res.status}: ${await res.text()}`);
1198
+ return chatToResponses(await res.json(), model);
1199
+ }
1200
+ if (account.provider === "copilot") {
1201
+ const openaiMessages = toOpenAIChatMessages(req.input, messages);
1202
+ const tools = buildOpenAIToolsForProvider(req, COMPUTER_USE_TYPES);
1203
+ const res = await callCopilot(account.oauthToken ?? "", model, openaiMessages, copilotInstructions(req, tools), false, signal, tools);
1204
+ if (!res.ok)
1205
+ throw new Error(`Copilot error ${res.status}: ${await res.text()}`);
1206
+ return chatToResponses(await res.json(), model);
1207
+ }
1208
+ // OpenAI
1209
+ account = await ensureFreshToken(account);
1210
+ if (account.method === "oauth-official") {
1211
+ if (!account.oauthToken)
1212
+ throw new Error("OpenAI OAuth token missing");
1213
+ const res = await callCodexAPI(account.oauthToken, buildCodexBody(req, model), signal);
1214
+ if (!res.ok)
1215
+ throw new Error(`Codex error ${res.status}: ${await res.text()}`);
1216
+ let fullText = "";
1217
+ for await (const delta of readSSE(res, codexExtract))
1218
+ fullText += delta;
1219
+ return { id: `resp_${Date.now()}`, object: "response", model, output: [{ type: "message", role: "assistant", content: [{ type: "output_text", text: fullText }] }] };
1220
+ }
1221
+ if (account.method === "oauth-unofficial") {
1222
+ const auth = accountToProviderAuth(account);
1223
+ return callOpenAIAndParse(auth, model, messages, instructions, signal);
1224
+ }
1225
+ const auth = accountToProviderAuth(account);
1226
+ const res = await callOpenAI(auth, model, messages, instructions, false, signal);
1227
+ if (!res.ok)
1228
+ throw new Error(`OpenAI error ${res.status}: ${await res.text()}`);
1229
+ return chatToResponses(await res.json(), model);
1230
+ }
1231
+ // ?�?� Single provider stream ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
1232
+ async function* streamSingleProvider(rawAccount, model, req, config, signal, usage = {}, responseId) {
1233
+ let account = rawAccount;
1234
+ const messages = parseInput(req.input);
1235
+ const { instructions } = req;
1236
+ if (account.provider === "anthropic") {
1237
+ account = await ensureFreshToken(account);
1238
+ const auth = accountToProviderAuth(account);
1239
+ const useThinking = supportsThinking(model);
1240
+ const cleanedInstructions = cleanInstructions(instructions);
1241
+ // Multi-turn tool use: reconstruct conversation from stored state
1242
+ let anthropicMessages;
1243
+ if (req.previous_response_id) {
1244
+ const prior = convStore.get(req.previous_response_id);
1245
+ if (prior) {
1246
+ anthropicMessages = [...prior.messages];
1247
+ const inputItems = Array.isArray(req.input) ? req.input : [];
1248
+ const toolResults = inputItems
1249
+ .filter(item => item.type === "function_call_output")
1250
+ .map(item => ({
1251
+ type: "tool_result",
1252
+ tool_use_id: item.call_id,
1253
+ content: typeof item.output === "string" ? item.output : JSON.stringify(item.output),
1254
+ }));
1255
+ if (toolResults.length > 0)
1256
+ anthropicMessages.push({ role: "user", content: toolResults });
1257
+ }
1258
+ else {
1259
+ anthropicMessages = messages;
1260
+ }
1261
+ }
1262
+ else {
1263
+ anthropicMessages = messages;
1264
+ }
1265
+ // Anthropic doesn't accept role "developer" (OpenAI inline system role) ??
1266
+ // fold developer messages into the system prompt and drop them from the list.
1267
+ const developerContent = anthropicMessages
1268
+ .filter(m => m.role === "developer")
1269
+ .map(m => typeof m.content === "string" ? m.content : m.content.map(c => c.text ?? "").join(""))
1270
+ .join("\n\n");
1271
+ const filteredMessages = anthropicMessages.filter(m => m.role === "user" || m.role === "assistant");
1272
+ const effectiveSystem = developerContent
1273
+ ? (cleanedInstructions ? `${cleanedInstructions}\n\n${developerContent}` : developerContent)
1274
+ : cleanedInstructions;
1275
+ // Filter to known-valid tools: function tools (have name) + computer_use types
1276
+ const rawTools = Array.isArray(req.tools) ? req.tools : [];
1277
+ const validTools = rawTools.filter(t => (typeof t.name === "string" && t.name.length > 0) ||
1278
+ COMPUTER_USE_TYPES.has(t.type));
1279
+ const anthropicTools = validTools.length > 0 ? validTools.map(toAnthropicTool) : undefined;
1280
+ const hasComputerUse = validTools.some(t => COMPUTER_USE_TYPES.has(t.type));
1281
+ const betas = hasComputerUse ? ["computer-use-2024-10-22"] : undefined;
1282
+ const capturedTools = [];
1283
+ const capturedText = { value: "" };
1284
+ const res = await callAnthropic(auth, model, filteredMessages, effectiveSystem, true, signal, useThinking, anthropicTools, betas);
1285
+ if (!res.ok)
1286
+ throw new Error(`Anthropic error ${res.status}: ${await res.text()}`);
1287
+ yield* readAnthropicSSEWithUsage(res, usage, capturedTools, capturedText);
1288
+ if (responseId && capturedTools.length > 0) {
1289
+ const assistantContent = [];
1290
+ if (capturedText.value)
1291
+ assistantContent.push({ type: "text", text: capturedText.value });
1292
+ for (const t of capturedTools)
1293
+ assistantContent.push({ type: "tool_use", id: t.id, name: t.name, input: t.input });
1294
+ convStore.set(responseId, {
1295
+ provider: "anthropic", model,
1296
+ messages: [...filteredMessages, { role: "assistant", content: assistantContent }],
1297
+ instructions: effectiveSystem, tools: req.tools, ts: Date.now(),
1298
+ });
1299
+ glog(`[gateway] conv stored: ${responseId} (${capturedTools.length} tools)`);
1300
+ }
1301
+ return;
1302
+ }
1303
+ if (account.provider === "google") {
1304
+ const auth = accountToProviderAuth(account);
1305
+ const hasToolInstructions = (Array.isArray(req.tools) && req.tools.length > 0) ||
1306
+ CODEX_TOOL_MARKERS.some(m => (instructions ?? "").includes(m));
1307
+ const googleSystem = hasToolInstructions
1308
+ ? "Use the provided function tools for all file, command, system, and web requests. Call tools directly without narrating what you plan to do."
1309
+ : cleanInstructions(instructions);
1310
+ const rawTools = Array.isArray(req.tools) ? req.tools : [];
1311
+ const googleTools = rawTools
1312
+ .filter(t => { const type = t.type; return !type || (!COMPUTER_USE_TYPES.has(type) && type !== "web_search"); })
1313
+ .map(toGoogleTool)
1314
+ .filter((t) => t !== null);
1315
+ const inputArr = Array.isArray(req.input) ? req.input : [];
1316
+ let googleContents;
1317
+ if (req.previous_response_id) {
1318
+ const prior = convStore.get(req.previous_response_id);
1319
+ if (prior?.googleContents) {
1320
+ const fnResponses = inputArr
1321
+ .filter(item => item.type === "function_call_output")
1322
+ .map(item => {
1323
+ const name = prior.googleToolCalls?.find(t => t.id === item.call_id)?.name ?? item.call_id;
1324
+ return { functionResponse: { name, response: { result: typeof item.output === "string" ? item.output : JSON.stringify(item.output) } } };
1325
+ });
1326
+ googleContents = fnResponses.length
1327
+ ? [...prior.googleContents, { role: "user", parts: fnResponses }]
1328
+ : [...prior.googleContents];
1329
+ }
1330
+ }
1331
+ const capturedTools = [];
1332
+ const capturedText = { value: "" };
1333
+ const res = await callGoogle(auth, model, messages, googleSystem, true, signal, googleTools, googleContents);
1334
+ if (!res.ok)
1335
+ throw new Error(`Google error ${res.status}: ${await res.text()}`);
1336
+ yield* readGoogleSSEWithUsage(res, usage, capturedTools, capturedText);
1337
+ if (responseId && capturedTools.length > 0) {
1338
+ const base = googleContents ?? messages.map((m) => ({
1339
+ role: m.role === "assistant" ? "model" : "user",
1340
+ parts: [{ text: typeof m.content === "string" ? m.content : m.content.map((c) => c.text ?? "").join("") }],
1341
+ }));
1342
+ const assistantParts = [];
1343
+ if (capturedText.value)
1344
+ assistantParts.push({ text: capturedText.value });
1345
+ for (const t of capturedTools)
1346
+ assistantParts.push({ functionCall: { name: t.name, args: t.input } });
1347
+ convStore.set(responseId, {
1348
+ provider: "google", model,
1349
+ messages: [],
1350
+ googleContents: [...base, { role: "model", parts: assistantParts }],
1351
+ googleToolCalls: capturedTools.map(t => ({ id: t.id, name: t.name })),
1352
+ instructions: googleSystem, tools: req.tools, ts: Date.now(),
1353
+ });
1354
+ glog(`[gateway] conv stored (google): ${responseId} (${capturedTools.length} tools)`);
1355
+ }
1356
+ return;
1357
+ }
1358
+ // Antigravity (Google Gemini via daily-cloudcode-pa.googleapis.com)
1359
+ if (account.provider === "antigravity") {
1360
+ account = await ensureFreshToken(account);
1361
+ const accessToken = account.oauthToken;
1362
+ if (!accessToken)
1363
+ throw new Error("Antigravity: no access token ??re-login required");
1364
+ // Fetch and persist projectId on first use if missing
1365
+ if (!account.projectId) {
1366
+ try {
1367
+ const projectId = await loadAntigravityProject(accessToken);
1368
+ account.projectId = projectId;
1369
+ const cfg = loadConfig();
1370
+ const stored = cfg.accounts.find(a => a.id === account.id);
1371
+ if (stored) {
1372
+ stored.projectId = projectId;
1373
+ saveConfig(cfg);
1374
+ }
1375
+ glog(`[gateway] antigravity: fetched projectId=${projectId}`);
1376
+ }
1377
+ catch (e) {
1378
+ gerr(`[gateway] antigravity: could not fetch projectId ??${e.message}`);
1379
+ }
1380
+ }
1381
+ const hasToolInstructions = (Array.isArray(req.tools) && req.tools.length > 0) ||
1382
+ CODEX_TOOL_MARKERS.some(m => (instructions ?? "").includes(m));
1383
+ const agSystem = hasToolInstructions
1384
+ ? "Use the provided function tools for all file, command, system, and web requests. Call tools directly without narrating what you plan to do."
1385
+ : cleanInstructions(instructions);
1386
+ const rawTools = Array.isArray(req.tools) ? req.tools : [];
1387
+ const agTools = rawTools
1388
+ .filter(t => { const type = t.type; return !type || (!COMPUTER_USE_TYPES.has(type) && type !== "web_search"); })
1389
+ .map(toGoogleTool)
1390
+ .filter((t) => t !== null);
1391
+ const inputArr = Array.isArray(req.input) ? req.input : [];
1392
+ const hasFnItems = inputArr.some(i => i.type === "function_call" || i.type === "function_call_output");
1393
+ let agContents;
1394
+ if (req.previous_response_id) {
1395
+ const prior = convStore.get(req.previous_response_id);
1396
+ if (prior?.googleContents) {
1397
+ const fnResponses = inputArr
1398
+ .filter(item => item.type === "function_call_output")
1399
+ .map(item => {
1400
+ const callId = item.call_id;
1401
+ const name = prior.googleToolCalls?.find(t => t.id === callId)?.name ?? callId ?? "tool";
1402
+ const output = typeof item.output === "string" ? item.output : JSON.stringify(item.output ?? "");
1403
+ return { functionResponse: { id: callId, name, response: { result: output } } };
1404
+ });
1405
+ agContents = fnResponses.length
1406
+ ? [...prior.googleContents, { role: "user", parts: fnResponses }]
1407
+ : [...prior.googleContents];
1408
+ glog(`[gateway] antigravity: using stored googleContents (thoughtSignature preserved)`);
1409
+ }
1410
+ }
1411
+ if (!agContents && hasFnItems) {
1412
+ const callIdToName = new Map();
1413
+ const contents = [];
1414
+ let pendingFnCalls = [];
1415
+ const flushModel = () => { if (pendingFnCalls.length) {
1416
+ contents.push({ role: "model", parts: pendingFnCalls });
1417
+ pendingFnCalls = [];
1418
+ } };
1419
+ for (const item of inputArr) {
1420
+ const type = item.type;
1421
+ if (type === "message") {
1422
+ flushModel();
1423
+ const content = typeof item.content === "string" ? item.content : (item.content ?? []).map(c => c.text ?? "").join("");
1424
+ contents.push({ role: item.role === "assistant" ? "model" : "user", parts: [{ text: content }] });
1425
+ }
1426
+ else if (type === "function_call") {
1427
+ const id = (item.id ?? item.call_id);
1428
+ const name = item.name;
1429
+ if (id && name)
1430
+ callIdToName.set(id, name);
1431
+ let args = {};
1432
+ try {
1433
+ args = typeof item.arguments === "string" ? JSON.parse(item.arguments) : (item.arguments ?? {});
1434
+ }
1435
+ catch { /**/ }
1436
+ pendingFnCalls.push({ functionCall: { id, name, args } });
1437
+ }
1438
+ else if (type === "function_call_output") {
1439
+ flushModel();
1440
+ const callId = item.call_id;
1441
+ const name = callIdToName.get(callId) ?? callId ?? "tool";
1442
+ const output = typeof item.output === "string" ? item.output : JSON.stringify(item.output ?? "");
1443
+ contents.push({ role: "user", parts: [{ functionResponse: { id: callId, name, response: { result: output } } }] });
1444
+ }
1445
+ }
1446
+ flushModel();
1447
+ agContents = contents;
1448
+ }
1449
+ const agFinalContents = agContents ?? messages.map(m => ({
1450
+ role: m.role === "assistant" ? "model" : "user",
1451
+ parts: [{ text: typeof m.content === "string" ? m.content : m.content.map(c => c.text ?? "").join("") }],
1452
+ }));
1453
+ const agSysInstruction = agSystem ? { parts: [{ text: agSystem }] } : undefined;
1454
+ const agCapturedTools = [];
1455
+ const agCapturedText = { value: "" };
1456
+ // Retry with progressively truncated tool outputs on context overflow
1457
+ let agCurrentContents = agFinalContents;
1458
+ let agRes = null;
1459
+ for (let attempt = 0; attempt < 5; attempt++) {
1460
+ agRes = await callAntigravity(accessToken, model, agCurrentContents, agSysInstruction, agTools, true, signal, account.projectId);
1461
+ if (agRes.ok)
1462
+ break;
1463
+ const errText = await agRes.text();
1464
+ const isOverflow = agRes.status === 400 && (errText.includes("token count exceeds") ||
1465
+ errText.includes("maximum context length") ||
1466
+ errText.includes("longer than the model's context length") ||
1467
+ errText.includes("exceed max_num_tokens") ||
1468
+ errText.includes("exceeds the model's maximum context"));
1469
+ if (!isOverflow)
1470
+ throw new Error(`Antigravity error ${agRes.status}: ${errText}`);
1471
+ // Halve the largest functionResponse outputs and retry
1472
+ const factor = 0.5 ** (attempt + 1);
1473
+ agCurrentContents = agCurrentContents.map((c) => {
1474
+ const msg = c;
1475
+ if (msg.role !== "user")
1476
+ return c;
1477
+ return {
1478
+ ...msg,
1479
+ parts: msg.parts.map(p => {
1480
+ if (!p.functionResponse?.response?.result)
1481
+ return p;
1482
+ const orig = p.functionResponse.response.result;
1483
+ const limit = Math.max(500, Math.floor(orig.length * factor));
1484
+ if (orig.length <= limit)
1485
+ return p;
1486
+ return { ...p, functionResponse: { ...p.functionResponse, response: { result: orig.slice(0, limit) + `\n[truncated to ${limit}/${orig.length} chars due to context limit]` } } };
1487
+ }),
1488
+ };
1489
+ });
1490
+ glog(`[gateway] antigravity context overflow ??retry ${attempt + 1} with tool outputs at ${Math.round(factor * 100)}%`);
1491
+ }
1492
+ if (!agRes.ok)
1493
+ throw new Error(`Antigravity error ${agRes.status}: context overflow after retries`);
1494
+ yield* readGoogleSSEWithUsage(agRes, usage, agCapturedTools, agCapturedText);
1495
+ if (responseId && agCapturedTools.length > 0) {
1496
+ const assistantParts = [];
1497
+ if (agCapturedText.value)
1498
+ assistantParts.push({ text: agCapturedText.value });
1499
+ for (const t of agCapturedTools) {
1500
+ const fc = { name: t.name, args: t.input };
1501
+ const part = { functionCall: fc };
1502
+ if (t.thoughtSignature)
1503
+ part.thoughtSignature = t.thoughtSignature;
1504
+ assistantParts.push(part);
1505
+ }
1506
+ convStore.set(responseId, {
1507
+ provider: "antigravity", model,
1508
+ messages: [],
1509
+ googleContents: [...agCurrentContents, { role: "model", parts: assistantParts }],
1510
+ googleToolCalls: agCapturedTools.map(t => ({ id: t.id, name: t.name })),
1511
+ instructions: agSystem, tools: req.tools, ts: Date.now(),
1512
+ });
1513
+ glog(`[gateway] conv stored (antigravity): ${responseId} (${agCapturedTools.length} tools)`);
1514
+ }
1515
+ return;
1516
+ }
1517
+ if (account.provider === "ollama") {
1518
+ // Keep Codex/skill instructions. Add bridge rules so local models use native
1519
+ // function calls instead of falling back to generic "no filesystem access" text.
1520
+ // Add bridge rules whenever tools are available (req.tools) OR instructions mention tools.
1521
+ // Previously only checked instructions, so models got no bridge rules when req.tools was
1522
+ // present but instructions had no tool marker text (common with qwen/non-English models).
1523
+ const hasToolInstructions = (Array.isArray(req.tools) && req.tools.length > 0) ||
1524
+ CODEX_TOOL_MARKERS.some(m => (instructions ?? "").includes(m));
1525
+ // For tool requests: use a short Ollama-optimized prompt instead of passing the full
1526
+ // Claude-specific Codex instructions. Those instructions confuse local models (they are
1527
+ // thousands of tokens of Claude-specific guidance) and bury the bridge rules at the end.
1528
+ const ollamaSystem = hasToolInstructions
1529
+ ? [
1530
+ "You are an AI assistant. Respond in the same language as the user.",
1531
+ "",
1532
+ "Tool use rules:",
1533
+ "- For file/command/system tasks: call the tool immediately. Do not narrate what you plan to do.",
1534
+ "- Parent directory = '../' path. Current directory = './' path.",
1535
+ "- File creation example: exec_command(cmd=\"echo '# Hello' > ../Hello.md\")",
1536
+ "- URL/web requests: use web_fetch tool ONLY. Never use curl/wget via exec_command.",
1537
+ "- After receiving a tool result: continue toward the original goal. Do not stop or ask questions until done.",
1538
+ "- Do NOT output XML, <function_calls>, or tool markup as text.",
1539
+ ].join("\n")
1540
+ : cleanInstructions(instructions);
1541
+ // Pass all Codex function tools to Ollama except web_search-typed and computer_use tools.
1542
+ // web_search is handled by the gateway web_fetch fallback below.
1543
+ // computer_use is a GUI tool that local models cannot execute.
1544
+ const SKIP_FOR_OLLAMA = new Set(["web_search", ...Array.from(COMPUTER_USE_TYPES)]);
1545
+ const ollamaTools = Array.isArray(req.tools) && req.tools.length > 0
1546
+ ? req.tools
1547
+ .filter(t => {
1548
+ const type = t.type;
1549
+ if (type && SKIP_FOR_OLLAMA.has(type))
1550
+ return false;
1551
+ return toolName(t) != null;
1552
+ })
1553
+ .map(toOpenAIFunctionTool)
1554
+ .filter((t) => t !== null)
1555
+ : undefined;
1556
+ // Check if Codex provides web_search with external access enabled.
1557
+ // When available, expose it to Ollama as an OpenAI function tool and let Codex execute it.
1558
+ // When not available, fall back to gateway-direct web_fetch.
1559
+ const codexWebSearch = Array.isArray(req.tools)
1560
+ ? req.tools.find(t => t.type === "web_search" && t.external_web_access === true)
1561
+ : undefined;
1562
+ const codexWebSearchTool = codexWebSearch ? {
1563
+ type: "function",
1564
+ function: {
1565
+ name: "web_search",
1566
+ description: "Search the web for current information. Use for news, facts, or any live internet data.",
1567
+ parameters: {
1568
+ type: "object",
1569
+ properties: { query: { type: "string", description: "Search query" } },
1570
+ required: ["query"],
1571
+ },
1572
+ },
1573
+ } : undefined;
1574
+ glog(`[ollama] web_search: ${codexWebSearch ? "codex(enabled)" : "gateway-fetch(fallback)"}`);
1575
+ // Multi-turn: Codex operates in stateless mode ??it sends the full conversation every turn
1576
+ // in req.input as an array of typed items (message, function_call, function_call_output).
1577
+ // We parse the full array and reconstruct OpenAI-format messages so the model sees tool results.
1578
+ let prebuiltMessages;
1579
+ let isToolResultTurn = false;
1580
+ let currentToolDepth = 0;
1581
+ const MAX_TOOL_TURNS = 4;
1582
+ const inputArr = Array.isArray(req.input) ? req.input : [];
1583
+ const hasFunctionItems = inputArr.some(i => i.type === "function_call" || i.type === "function_call_output");
1584
+ if (hasFunctionItems) {
1585
+ // Stateless multi-turn: reconstruct full conversation from input array
1586
+ currentToolDepth = inputArr.filter(i => i.type === "function_call").length;
1587
+ glog(`[ollama] stateless turn: depth=${currentToolDepth} items=${inputArr.length}`);
1588
+ if (currentToolDepth > MAX_TOOL_TURNS) {
1589
+ // Too many rounds ??force text-only summarize
1590
+ isToolResultTurn = true;
1591
+ const origQ = inputArr.find(i => i.type === "message" && i.role === "user");
1592
+ const origQuestion = typeof origQ?.content === "string" ? origQ.content
1593
+ : (origQ?.content ?? []).map(c => c.text ?? "").join("");
1594
+ const resultsText = inputArr
1595
+ .filter(i => i.type === "function_call_output")
1596
+ .map(i => parseCodexToolOutput(i.output))
1597
+ .join("\n---\n");
1598
+ prebuiltMessages = [
1599
+ { role: "system", content: "Answer in the same language as the user. Do NOT call any tools. Respond with text only." },
1600
+ { role: "user", content: `User question: ${origQuestion}\n\nTool results:\n${resultsText}\n\nBased on the above results, answer the user's question concisely.` },
1601
+ ];
1602
+ }
1603
+ else {
1604
+ // Build OpenAI-format history: message ??function_call ??function_call_output ????
1605
+ const msgs = [{ role: "system", content: ollamaSystem }];
1606
+ let pendingToolCalls = [];
1607
+ let pendingText = "";
1608
+ const flushAssistant = () => {
1609
+ if (pendingToolCalls.length > 0 || pendingText) {
1610
+ msgs.push({ role: "assistant", content: pendingText || null, ...(pendingToolCalls.length > 0 ? { tool_calls: pendingToolCalls } : {}) });
1611
+ pendingToolCalls = [];
1612
+ pendingText = "";
1613
+ }
1614
+ };
1615
+ for (const item of inputArr) {
1616
+ const type = item.type;
1617
+ if (type === "message") {
1618
+ flushAssistant();
1619
+ const content = typeof item.content === "string" ? item.content
1620
+ : (item.content ?? []).map(c => c.text ?? "").join("");
1621
+ const role = item.role || "user";
1622
+ if (role === "assistant") {
1623
+ pendingText += content;
1624
+ }
1625
+ else {
1626
+ msgs.push({ role, content });
1627
+ }
1628
+ }
1629
+ else if (type === "function_call") {
1630
+ pendingToolCalls.push({
1631
+ id: item.id ?? `call_${Date.now()}`,
1632
+ type: "function",
1633
+ function: {
1634
+ name: item.name,
1635
+ arguments: typeof item.arguments === "string" ? item.arguments : JSON.stringify(item.arguments ?? {}),
1636
+ },
1637
+ });
1638
+ }
1639
+ else if (type === "function_call_output") {
1640
+ flushAssistant();
1641
+ msgs.push({ role: "tool", tool_call_id: item.call_id, content: parseCodexToolOutput(item.output) });
1642
+ }
1643
+ }
1644
+ flushAssistant();
1645
+ prebuiltMessages = msgs;
1646
+ }
1647
+ }
1648
+ else if (req.previous_response_id) {
1649
+ // Stateful fallback (previous_response_id present, no function items in input)
1650
+ const prior = convStore.get(req.previous_response_id);
1651
+ if (prior?.openaiMessages) {
1652
+ prebuiltMessages = [...prior.openaiMessages];
1653
+ }
1654
+ }
1655
+ // If Codex web_search is not available, fall back to gateway-direct web_fetch.
1656
+ const webFetchSchema = {
1657
+ type: "function",
1658
+ function: {
1659
+ name: "web_fetch",
1660
+ description: "Fetch and read the text content of any URL from the internet. Use whenever the user asks to visit a website or read a specific URL.",
1661
+ parameters: { type: "object", properties: { url: { type: "string", description: "Full URL to fetch" } }, required: ["url"] },
1662
+ },
1663
+ };
1664
+ // GATEWAY_TOOL_NAMES is empty: web_fetch function_call events now flow to Codex (for gray-section display),
1665
+ // while the gateway ALSO executes the fetch internally via the loop below.
1666
+ const GATEWAY_TOOL_NAMES = new Set();
1667
+ const MAX_WEB_FETCHES = 2;
1668
+ let gatewayFetchCount = 0;
1669
+ // Don't offer tools in the summarize turn ??model must answer in text.
1670
+ // Ollama always uses gateway web_fetch ??Codex web_search is unreliable for local models.
1671
+ const turnTools = isToolResultTurn ? undefined : [...(ollamaTools ?? []), webFetchSchema];
1672
+ // Build initial conversation history for the gateway tool loop.
1673
+ const loopInitialMsgs = prebuiltMessages ?? (() => {
1674
+ const msgs = [];
1675
+ if (ollamaSystem)
1676
+ msgs.push({ role: "system", content: ollamaSystem });
1677
+ msgs.push(...messages.map(m => ({
1678
+ role: m.role,
1679
+ content: typeof m.content === "string" ? m.content
1680
+ : m.content.map(c => c.text ?? "").join(""),
1681
+ })));
1682
+ return msgs;
1683
+ })();
1684
+ let loopMsgs = loopInitialMsgs;
1685
+ let capturedOllamaTools = [];
1686
+ let capturedOllamaText = { value: "" };
1687
+ // Open reasoning item early so Codex idle timer stays alive during model load
1688
+ yield { type: 'reasoning', content: '' };
1689
+ while (true) {
1690
+ capturedOllamaTools = [];
1691
+ capturedOllamaText = { value: "" };
1692
+ const res = await callOllama(config.ollamaBaseUrl, model, messages, ollamaSystem, true, signal, turnTools, loopMsgs);
1693
+ if (!res.ok)
1694
+ throw new Error(`Ollama error ${res.status}: ${await res.text()}`);
1695
+ // Stream chunks to server.ts, suppressing tool_call events for gateway-handled tools
1696
+ const gatewayCallIds = new Set();
1697
+ for await (const chunk of readOpenAISSEWithUsage(res, usage, capturedOllamaTools, capturedOllamaText, isToolResultTurn)) {
1698
+ if (chunk.type === 'tool_call_start' && GATEWAY_TOOL_NAMES.has(chunk.name)) {
1699
+ gatewayCallIds.add(chunk.id);
1700
+ continue;
1701
+ }
1702
+ if ((chunk.type === 'tool_call_delta' || chunk.type === 'tool_call_end') && gatewayCallIds.has(chunk.id)) {
1703
+ continue;
1704
+ }
1705
+ yield chunk;
1706
+ }
1707
+ const webFetchCalls = capturedOllamaTools.filter(t => t.name === "web_fetch");
1708
+ const codexToolCalls = capturedOllamaTools.filter(t => t.name !== "web_fetch");
1709
+ if (webFetchCalls.length === 0 || gatewayFetchCount >= MAX_WEB_FETCHES)
1710
+ break;
1711
+ gatewayFetchCount += webFetchCalls.length;
1712
+ // Append assistant turn with all tool calls to conversation history
1713
+ loopMsgs = [
1714
+ ...loopMsgs,
1715
+ {
1716
+ role: "assistant",
1717
+ content: capturedOllamaText.value || null,
1718
+ tool_calls: capturedOllamaTools.map(t => ({
1719
+ id: t.id, type: "function",
1720
+ function: { name: t.name, arguments: t.rawArgs ?? JSON.stringify(t.input) },
1721
+ })),
1722
+ },
1723
+ ];
1724
+ // Execute each web_fetch via the gateway and append results
1725
+ for (const call of webFetchCalls) {
1726
+ const url = (call.input.url ?? call.input.uri ?? call.input.link ?? "").trim();
1727
+ // web_fetch status already emitted as text chunk at tool_call_end above
1728
+ let content;
1729
+ try {
1730
+ const webRes = await fetch(url, {
1731
+ signal: AbortSignal.timeout(15_000),
1732
+ headers: { "User-Agent": "Mozilla/5.0 (compatible; rcodex-Gateway/1.0)" },
1733
+ });
1734
+ const text = await webRes.text();
1735
+ // Extract title and meta description first ??most informative even on JS-heavy sites
1736
+ const title = text.match(/<title[^>]*>([^<]*)<\/title>/i)?.[1]?.trim() ?? "";
1737
+ const metaDesc = (text.match(/<meta[^>]+name=["']description["'][^>]+content=["']([^"']+)["']/i)?.[1] ??
1738
+ text.match(/<meta[^>]+content=["']([^"']+)["'][^>]+name=["']description["']/i)?.[1] ??
1739
+ text.match(/<meta[^>]+property=["']og:description["'][^>]+content=["']([^"']+)["']/i)?.[1] ??
1740
+ "").trim();
1741
+ const header = [title && `?�목: ${title}`, metaDesc && `?�명: ${metaDesc}`].filter(Boolean).join("\n");
1742
+ const body = text
1743
+ .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
1744
+ .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
1745
+ .replace(/<[^>]+>/g, " ")
1746
+ .replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&")
1747
+ .replace(/\s+/g, " ").trim()
1748
+ .slice(0, 12000);
1749
+ content = header ? `${header}\n\n${body}` : body;
1750
+ if (text.length > 12000)
1751
+ content += "\n...(?�용 ?�림)";
1752
+ }
1753
+ catch (e) {
1754
+ content = `가?�오�??�패: ${e instanceof Error ? e.message : String(e)}`;
1755
+ }
1756
+ loopMsgs.push({ role: "tool", tool_call_id: call.id, content });
1757
+ }
1758
+ if (codexToolCalls.length > 0)
1759
+ break; // Codex tools also called ??let Codex handle them
1760
+ }
1761
+ glog(`[ollama] turn done: tools=${capturedOllamaTools.filter(t => t.name !== "web_fetch").length} text=${capturedOllamaText.value.length}chars isResultTurn=${isToolResultTurn} preview="${capturedOllamaText.value.slice(0, 80).replace(/\n/g, ' ')}"`);
1762
+ const currentToolOutputs = (Array.isArray(req.input) ? req.input : [])
1763
+ .filter(item => item.type === "function_call_output");
1764
+ if (capturedOllamaTools.length === 0 && capturedOllamaText.value.trim() === "" && currentToolOutputs.length > 0) {
1765
+ // Model returned empty after tool result ??synthesize a brief summary from the output.
1766
+ const lastOutput = currentToolOutputs[currentToolOutputs.length - 1];
1767
+ const parsed = parseCodexToolOutput(lastOutput?.output);
1768
+ let fallbackText;
1769
+ if (!parsed || parsed.includes("결과 ?�음") || parsed.includes("?�당 ?�일/??��??존재?��? ?�습?�다")) {
1770
+ fallbackText = parsed || "명령???�행?��?�?결과가 ?�습?�다.";
1771
+ }
1772
+ else {
1773
+ // Trim long listings ??show first 20 lines so it's not a wall of text
1774
+ const lines = parsed.split("\n");
1775
+ const preview = lines.slice(0, 20).join("\n");
1776
+ const more = lines.length > 20 ? `\n... (${lines.length - 20}�???` : "";
1777
+ fallbackText = preview + more;
1778
+ }
1779
+ capturedOllamaText.value += fallbackText;
1780
+ yield { type: "text", content: fallbackText };
1781
+ }
1782
+ if (responseId && capturedOllamaTools.length > 0) {
1783
+ // Build OpenAI-format assistant message for next turn
1784
+ const assistantMsg = {
1785
+ role: "assistant",
1786
+ content: capturedOllamaText.value || null,
1787
+ tool_calls: capturedOllamaTools.map(t => ({
1788
+ id: t.id, type: "function",
1789
+ function: { name: t.name, arguments: t.rawArgs ?? JSON.stringify(t.input) },
1790
+ })),
1791
+ };
1792
+ const base = prebuiltMessages ?? (() => {
1793
+ const msgs = [];
1794
+ if (ollamaSystem)
1795
+ msgs.push({ role: "system", content: ollamaSystem });
1796
+ msgs.push(...messages.map(m => ({ role: m.role, content: typeof m.content === "string" ? m.content : m.content.map(c => c.text ?? "").join("") })));
1797
+ return msgs;
1798
+ })();
1799
+ convStore.set(responseId, {
1800
+ provider: "ollama", model,
1801
+ messages: [],
1802
+ openaiMessages: [...base, assistantMsg],
1803
+ toolTurnDepth: currentToolDepth,
1804
+ instructions: ollamaSystem, tools: req.tools, ts: Date.now(),
1805
+ });
1806
+ glog(`[gateway] conv stored (ollama): ${responseId} (${capturedOllamaTools.length} tools, depth=${currentToolDepth})`);
1807
+ }
1808
+ return;
1809
+ }
1810
+ if (account.provider === "copilot") {
1811
+ const openaiMessages = toOpenAIChatMessages(req.input, messages);
1812
+ const tools = buildOpenAIToolsForProvider(req, COMPUTER_USE_TYPES);
1813
+ const capturedCopilotTools = [];
1814
+ const capturedCopilotText = { value: "" };
1815
+ const res = await callCopilot(account.oauthToken ?? "", model, openaiMessages, copilotInstructions(req, tools), true, signal, tools);
1816
+ if (!res.ok)
1817
+ throw new Error(`Copilot error ${res.status}: ${await res.text()}`);
1818
+ yield* readOpenAISSEWithUsage(res, usage, capturedCopilotTools, capturedCopilotText);
1819
+ if (responseId && capturedCopilotTools.length > 0) {
1820
+ convStore.set(responseId, {
1821
+ provider: "copilot", model,
1822
+ messages: [],
1823
+ openaiMessages: [
1824
+ ...openaiMessages,
1825
+ {
1826
+ role: "assistant",
1827
+ content: capturedCopilotText.value || null,
1828
+ tool_calls: capturedCopilotTools.map(t => ({
1829
+ id: t.id,
1830
+ type: "function",
1831
+ function: { name: t.name, arguments: t.rawArgs ?? JSON.stringify(t.input) },
1832
+ })),
1833
+ },
1834
+ ],
1835
+ instructions: copilotInstructions(req, tools), tools: req.tools, ts: Date.now(),
1836
+ });
1837
+ glog(`[gateway] conv stored (copilot): ${responseId} (${capturedCopilotTools.length} tools)`);
1838
+ }
1839
+ return;
1840
+ }
1841
+ // OpenAI
1842
+ account = await ensureFreshToken(account);
1843
+ if (account.method === "oauth-official") {
1844
+ if (!account.oauthToken)
1845
+ throw new Error("OpenAI OAuth token missing");
1846
+ const res = await callCodexAPI(account.oauthToken, buildCodexBody(req, model), signal);
1847
+ if (!res.ok)
1848
+ throw new Error(`Codex error ${res.status}: ${await res.text()}`);
1849
+ yield* readCodexSSEWithUsage(res, usage);
1850
+ return;
1851
+ }
1852
+ if (account.method === "oauth-unofficial") {
1853
+ const result = await callSingleProvider(account, model, req, config, signal);
1854
+ const text = result.output[0]?.content[0]?.text ?? "";
1855
+ if (text)
1856
+ yield { type: 'text', content: text };
1857
+ return;
1858
+ }
1859
+ const auth = accountToProviderAuth(account);
1860
+ const res = await callOpenAI(auth, model, messages, instructions, true, signal);
1861
+ if (!res.ok)
1862
+ throw new Error(`OpenAI error ${res.status}: ${await res.text()}`);
1863
+ yield* readOpenAISSEWithUsage(res, usage);
1864
+ }
1865
+ // ?�?� Streaming proxy with fallback ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
1866
+ export async function* streamProxyRequest(req, config, signal, responseId) {
1867
+ const candidates = resolveAccounts(req.model, config);
1868
+ if (!candidates.length)
1869
+ throw new Error("No provider connected to output");
1870
+ const order = candidates.map((c, i) => `${i + 1}.${c.account.provider}(${c.model})`).join(" ??");
1871
+ glog(`[gateway] stream routing order: ${order}`);
1872
+ const t0 = Date.now();
1873
+ let lastError = null;
1874
+ const failedModels = [];
1875
+ for (let i = 0; i < candidates.length; i++) {
1876
+ const { account, model } = candidates[i];
1877
+ let yielded = false;
1878
+ let entry = null;
1879
+ const usage = {};
1880
+ try {
1881
+ for await (const chunk of streamSingleProvider(account, model, req, config, signal, usage, responseId)) {
1882
+ if (!yielded) {
1883
+ if (i > 0)
1884
+ gwarn(`[gateway] ??stream fallback: ${account.provider}(${model})`);
1885
+ else
1886
+ glog(`[gateway] ??stream ${account.provider}(${model})`);
1887
+ entry = pushLog({ ts: Date.now(), requestedModel: req.model, provider: account.provider, usedModel: model, fallback: i > 0, failedModels: failedModels.length ? [...failedModels] : undefined, ms: Date.now() - t0, status: "ok" });
1888
+ yielded = true;
1889
+ }
1890
+ yield chunk;
1891
+ }
1892
+ // Update entry with usage + final latency, then persist
1893
+ if (entry) {
1894
+ entry.ms = Date.now() - t0;
1895
+ if (usage.inputTokens)
1896
+ entry.inputTokens = usage.inputTokens;
1897
+ if (usage.outputTokens)
1898
+ entry.outputTokens = usage.outputTokens;
1899
+ }
1900
+ flushLog();
1901
+ return;
1902
+ }
1903
+ catch (err) {
1904
+ if (yielded)
1905
+ throw err;
1906
+ lastError = err instanceof Error ? err : new Error(String(err));
1907
+ failedModels.push(model);
1908
+ gerr(`[gateway] ??stream ${account.provider}(${model}) failed: ${lastError.message}${i < candidates.length - 1 ? " ??trying next" : ""}`);
1909
+ }
1910
+ }
1911
+ const lastCand = candidates[candidates.length - 1];
1912
+ pushLog({ ts: Date.now(), requestedModel: req.model, provider: lastCand?.account.provider ?? "", usedModel: "", fallback: false, failedModels: failedModels.length ? [...failedModels] : undefined, ms: Date.now() - t0, status: "error", error: lastError?.message });
1913
+ flushLog();
1914
+ throw lastError ?? new Error("All providers failed");
1915
+ }
1916
+ // ?�?� Non-streaming proxy with fallback ?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�?�
1917
+ export async function proxyRequest(req, config, signal) {
1918
+ const candidates = resolveAccounts(req.model, config);
1919
+ if (!candidates.length)
1920
+ throw new Error("No provider connected to output");
1921
+ const order = candidates.map((c, i) => `${i + 1}.${c.account.provider}(${c.model})`).join(" ??");
1922
+ glog(`[gateway] routing order: ${order}`);
1923
+ const t0 = Date.now();
1924
+ let lastError = null;
1925
+ const failedModels = [];
1926
+ for (let i = 0; i < candidates.length; i++) {
1927
+ const { account, model } = candidates[i];
1928
+ try {
1929
+ const result = await callSingleProvider(account, model, req, config, signal);
1930
+ if (i > 0)
1931
+ gwarn(`[gateway] ??fallback used: ${account.provider}(${model}) (primary failed)`);
1932
+ else
1933
+ glog(`[gateway] ??${account.provider}(${model})`);
1934
+ pushLog({ ts: Date.now(), requestedModel: req.model, provider: account.provider, usedModel: model, fallback: i > 0, failedModels: failedModels.length ? [...failedModels] : undefined, ms: Date.now() - t0, status: "ok", inputTokens: result.usage?.input_tokens, outputTokens: result.usage?.output_tokens });
1935
+ flushLog();
1936
+ return result;
1937
+ }
1938
+ catch (err) {
1939
+ lastError = err instanceof Error ? err : new Error(String(err));
1940
+ failedModels.push(model);
1941
+ gerr(`[gateway] ??${account.provider}(${model}) failed: ${lastError.message}${i < candidates.length - 1 ? " ??trying next" : ""}`);
1942
+ }
1943
+ }
1944
+ const lastCandNS = candidates[candidates.length - 1];
1945
+ pushLog({ ts: Date.now(), requestedModel: req.model, provider: lastCandNS?.account.provider ?? "", usedModel: "", fallback: false, failedModels: failedModels.length ? [...failedModels] : undefined, ms: Date.now() - t0, status: "error", error: lastError?.message });
1946
+ flushLog();
1947
+ throw lastError ?? new Error("All providers failed");
1948
+ }
1949
+ //# sourceMappingURL=proxy.js.map