@uncensoredcode/openbridge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +117 -0
  2. package/bin/openbridge.js +10 -0
  3. package/package.json +85 -0
  4. package/packages/cli/dist/args.d.ts +30 -0
  5. package/packages/cli/dist/args.js +160 -0
  6. package/packages/cli/dist/cli.d.ts +2 -0
  7. package/packages/cli/dist/cli.js +9 -0
  8. package/packages/cli/dist/index.d.ts +26 -0
  9. package/packages/cli/dist/index.js +76 -0
  10. package/packages/runtime/dist/assistant-protocol.d.ts +34 -0
  11. package/packages/runtime/dist/assistant-protocol.js +121 -0
  12. package/packages/runtime/dist/execution/in-process.d.ts +14 -0
  13. package/packages/runtime/dist/execution/in-process.js +45 -0
  14. package/packages/runtime/dist/execution/types.d.ts +49 -0
  15. package/packages/runtime/dist/execution/types.js +20 -0
  16. package/packages/runtime/dist/index.d.ts +86 -0
  17. package/packages/runtime/dist/index.js +60 -0
  18. package/packages/runtime/dist/normalizers/index.d.ts +6 -0
  19. package/packages/runtime/dist/normalizers/index.js +12 -0
  20. package/packages/runtime/dist/normalizers/legacy-packet.d.ts +6 -0
  21. package/packages/runtime/dist/normalizers/legacy-packet.js +131 -0
  22. package/packages/runtime/dist/output-sanitizer.d.ts +23 -0
  23. package/packages/runtime/dist/output-sanitizer.js +78 -0
  24. package/packages/runtime/dist/packet-extractor.d.ts +17 -0
  25. package/packages/runtime/dist/packet-extractor.js +43 -0
  26. package/packages/runtime/dist/packet-normalizer.d.ts +21 -0
  27. package/packages/runtime/dist/packet-normalizer.js +47 -0
  28. package/packages/runtime/dist/prompt-compiler.d.ts +28 -0
  29. package/packages/runtime/dist/prompt-compiler.js +301 -0
  30. package/packages/runtime/dist/protocol.d.ts +44 -0
  31. package/packages/runtime/dist/protocol.js +165 -0
  32. package/packages/runtime/dist/provider-failure.d.ts +52 -0
  33. package/packages/runtime/dist/provider-failure.js +236 -0
  34. package/packages/runtime/dist/provider.d.ts +40 -0
  35. package/packages/runtime/dist/provider.js +1 -0
  36. package/packages/runtime/dist/runtime.d.ts +86 -0
  37. package/packages/runtime/dist/runtime.js +462 -0
  38. package/packages/runtime/dist/session-bound-provider.d.ts +52 -0
  39. package/packages/runtime/dist/session-bound-provider.js +366 -0
  40. package/packages/runtime/dist/tool-name-aliases.d.ts +5 -0
  41. package/packages/runtime/dist/tool-name-aliases.js +13 -0
  42. package/packages/runtime/dist/tools/bash.d.ts +9 -0
  43. package/packages/runtime/dist/tools/bash.js +157 -0
  44. package/packages/runtime/dist/tools/edit.d.ts +9 -0
  45. package/packages/runtime/dist/tools/edit.js +94 -0
  46. package/packages/runtime/dist/tools/index.d.ts +39 -0
  47. package/packages/runtime/dist/tools/index.js +27 -0
  48. package/packages/runtime/dist/tools/list-dir.d.ts +9 -0
  49. package/packages/runtime/dist/tools/list-dir.js +127 -0
  50. package/packages/runtime/dist/tools/read.d.ts +9 -0
  51. package/packages/runtime/dist/tools/read.js +56 -0
  52. package/packages/runtime/dist/tools/registry.d.ts +15 -0
  53. package/packages/runtime/dist/tools/registry.js +38 -0
  54. package/packages/runtime/dist/tools/runtime-path.d.ts +7 -0
  55. package/packages/runtime/dist/tools/runtime-path.js +22 -0
  56. package/packages/runtime/dist/tools/search-files.d.ts +9 -0
  57. package/packages/runtime/dist/tools/search-files.js +149 -0
  58. package/packages/runtime/dist/tools/text-file.d.ts +32 -0
  59. package/packages/runtime/dist/tools/text-file.js +101 -0
  60. package/packages/runtime/dist/tools/workspace-path.d.ts +17 -0
  61. package/packages/runtime/dist/tools/workspace-path.js +70 -0
  62. package/packages/runtime/dist/tools/write.d.ts +9 -0
  63. package/packages/runtime/dist/tools/write.js +59 -0
  64. package/packages/server/dist/bridge/bridge-model-catalog.d.ts +56 -0
  65. package/packages/server/dist/bridge/bridge-model-catalog.js +100 -0
  66. package/packages/server/dist/bridge/bridge-runtime-service.d.ts +61 -0
  67. package/packages/server/dist/bridge/bridge-runtime-service.js +1386 -0
  68. package/packages/server/dist/bridge/chat-completions/chat-completion-service.d.ts +127 -0
  69. package/packages/server/dist/bridge/chat-completions/chat-completion-service.js +1026 -0
  70. package/packages/server/dist/bridge/index.d.ts +335 -0
  71. package/packages/server/dist/bridge/index.js +45 -0
  72. package/packages/server/dist/bridge/live-provider-extraction-canary.d.ts +69 -0
  73. package/packages/server/dist/bridge/live-provider-extraction-canary.js +186 -0
  74. package/packages/server/dist/bridge/providers/generic-provider-transport.d.ts +53 -0
  75. package/packages/server/dist/bridge/providers/generic-provider-transport.js +973 -0
  76. package/packages/server/dist/bridge/providers/provider-session-resolver.d.ts +17 -0
  77. package/packages/server/dist/bridge/providers/provider-session-resolver.js +95 -0
  78. package/packages/server/dist/bridge/providers/provider-streams.d.ts +80 -0
  79. package/packages/server/dist/bridge/providers/provider-streams.js +844 -0
  80. package/packages/server/dist/bridge/providers/provider-transport-profile.d.ts +194 -0
  81. package/packages/server/dist/bridge/providers/provider-transport-profile.js +198 -0
  82. package/packages/server/dist/bridge/providers/web-provider-transport.d.ts +30 -0
  83. package/packages/server/dist/bridge/providers/web-provider-transport.js +151 -0
  84. package/packages/server/dist/bridge/state/file-bridge-state-store.d.ts +36 -0
  85. package/packages/server/dist/bridge/state/file-bridge-state-store.js +164 -0
  86. package/packages/server/dist/bridge/stores/local-session-package-store.d.ts +23 -0
  87. package/packages/server/dist/bridge/stores/local-session-package-store.js +548 -0
  88. package/packages/server/dist/bridge/stores/provider-store.d.ts +94 -0
  89. package/packages/server/dist/bridge/stores/provider-store.js +143 -0
  90. package/packages/server/dist/bridge/stores/session-backed-provider-store.d.ts +7 -0
  91. package/packages/server/dist/bridge/stores/session-backed-provider-store.js +26 -0
  92. package/packages/server/dist/bridge/stores/session-package-store.d.ts +286 -0
  93. package/packages/server/dist/bridge/stores/session-package-store.js +1527 -0
  94. package/packages/server/dist/bridge/stores/session-store.d.ts +120 -0
  95. package/packages/server/dist/bridge/stores/session-store.js +139 -0
  96. package/packages/server/dist/cli/index.d.ts +9 -0
  97. package/packages/server/dist/cli/index.js +6 -0
  98. package/packages/server/dist/cli/main.d.ts +2 -0
  99. package/packages/server/dist/cli/main.js +9 -0
  100. package/packages/server/dist/cli/run-bridge-server-cli.d.ts +54 -0
  101. package/packages/server/dist/cli/run-bridge-server-cli.js +371 -0
  102. package/packages/server/dist/client/bridge-api-client.d.ts +61 -0
  103. package/packages/server/dist/client/bridge-api-client.js +267 -0
  104. package/packages/server/dist/client/index.d.ts +11 -0
  105. package/packages/server/dist/client/index.js +11 -0
  106. package/packages/server/dist/config/bridge-server-config.d.ts +52 -0
  107. package/packages/server/dist/config/bridge-server-config.js +118 -0
  108. package/packages/server/dist/config/index.d.ts +20 -0
  109. package/packages/server/dist/config/index.js +8 -0
  110. package/packages/server/dist/http/bridge-api-route-context.d.ts +14 -0
  111. package/packages/server/dist/http/bridge-api-route-context.js +1 -0
  112. package/packages/server/dist/http/create-bridge-api-server.d.ts +72 -0
  113. package/packages/server/dist/http/create-bridge-api-server.js +225 -0
  114. package/packages/server/dist/http/index.d.ts +5 -0
  115. package/packages/server/dist/http/index.js +5 -0
  116. package/packages/server/dist/http/parse-request.d.ts +6 -0
  117. package/packages/server/dist/http/parse-request.js +27 -0
  118. package/packages/server/dist/http/register-bridge-api-routes.d.ts +7 -0
  119. package/packages/server/dist/http/register-bridge-api-routes.js +17 -0
  120. package/packages/server/dist/http/routes/admin-routes.d.ts +7 -0
  121. package/packages/server/dist/http/routes/admin-routes.js +135 -0
  122. package/packages/server/dist/http/routes/chat-completions-route.d.ts +7 -0
  123. package/packages/server/dist/http/routes/chat-completions-route.js +49 -0
  124. package/packages/server/dist/http/routes/health-routes.d.ts +6 -0
  125. package/packages/server/dist/http/routes/health-routes.js +7 -0
  126. package/packages/server/dist/http/routes/message-routes.d.ts +7 -0
  127. package/packages/server/dist/http/routes/message-routes.js +7 -0
  128. package/packages/server/dist/index.d.ts +85 -0
  129. package/packages/server/dist/index.js +28 -0
  130. package/packages/server/dist/security/bridge-auth.d.ts +9 -0
  131. package/packages/server/dist/security/bridge-auth.js +41 -0
  132. package/packages/server/dist/security/cors-policy.d.ts +5 -0
  133. package/packages/server/dist/security/cors-policy.js +34 -0
  134. package/packages/server/dist/security/index.d.ts +16 -0
  135. package/packages/server/dist/security/index.js +12 -0
  136. package/packages/server/dist/security/redact-sensitive-values.d.ts +19 -0
  137. package/packages/server/dist/security/redact-sensitive-values.js +67 -0
  138. package/packages/server/dist/shared/api-schema.d.ts +133 -0
  139. package/packages/server/dist/shared/api-schema.js +1 -0
  140. package/packages/server/dist/shared/bridge-api-error.d.ts +17 -0
  141. package/packages/server/dist/shared/bridge-api-error.js +19 -0
  142. package/packages/server/dist/shared/index.d.ts +7 -0
  143. package/packages/server/dist/shared/index.js +7 -0
  144. package/packages/server/dist/shared/output.d.ts +5 -0
  145. package/packages/server/dist/shared/output.js +14 -0
@@ -0,0 +1,1026 @@
1
+ import crypto from "node:crypto";
2
+ import { bridgeRuntime } from "@uncensoredcode/openbridge/runtime";
3
+ import { z } from "zod";
4
+ import { bridgeApiErrorModule } from "../../shared/bridge-api-error.js";
5
+ import { bridgeModelCatalogModule } from "../bridge-model-catalog.js";
6
+ import { providerStreamsModule } from "../providers/provider-streams.js";
7
+ import { fileBridgeStateStoreModule } from "../state/file-bridge-state-store.js";
8
+ const { normalizeProviderToolName } = bridgeRuntime;
9
+ const { BridgeApiError } = bridgeApiErrorModule;
10
+ const { resolveBridgeModel } = bridgeModelCatalogModule;
11
+ const { extractIncrementalPacketMessage } = providerStreamsModule;
12
+ const { FileBridgeStateStore } = fileBridgeStateStoreModule;
13
+ const MAX_TOOL_AWARE_SYSTEM_MESSAGE_CHARS = 4000;
14
+ const EXPLICIT_CHAT_COMPLETION_METADATA_KEYS = [
15
+ "sessionID",
16
+ "sessionId",
17
+ "session_id",
18
+ "conversationID",
19
+ "conversationId",
20
+ "conversation_id",
21
+ "chatID",
22
+ "chatId",
23
+ "chat_id",
24
+ "threadID",
25
+ "threadId",
26
+ "thread_id"
27
+ ];
28
+ const EXPLICIT_CHAT_COMPLETION_HEADER_KEYS = [
29
+ "x-bridge-session-id",
30
+ "x-bridge-conversation-id",
31
+ "x-bridge-chat-id",
32
+ "x-bridge-thread-id",
33
+ "x-session-id",
34
+ "x-conversation-id",
35
+ "x-chat-id",
36
+ "x-thread-id"
37
+ ];
38
+ async function handleBridgeChatCompletionRequest(input) {
39
+ const body = input.body;
40
+ assertSupportedChatCompletionRequest(body);
41
+ const resolvedModel = resolveBridgeModel(input.providerStore.list(), body.model);
42
+ if (!resolvedModel) {
43
+ throw new BridgeApiError({
44
+ statusCode: 404,
45
+ code: "model_not_found",
46
+ message: `Model '${body.model}' was not found in the bridge model catalog.`
47
+ });
48
+ }
49
+ if (!resolvedModel.available) {
50
+ throw new BridgeApiError({
51
+ statusCode: 409,
52
+ code: "provider_unavailable",
53
+ message: `Provider '${resolvedModel.provider.id}' is disabled for model '${body.model}'.`
54
+ });
55
+ }
56
+ const completionId = crypto.randomUUID();
57
+ const completionObjectId = `chatcmpl_${completionId}`;
58
+ const bridgeSessionId = (await resolveChatCompletionBridgeSessionId(input.stateStore, resolvedModel.provider.id, body, input.headers)) ?? `chatcmpl:${completionId}`;
59
+ const providerBinding = await input.stateStore.loadBinding(resolvedModel.provider.id, bridgeSessionId);
60
+ const created = Math.floor((input.now ?? Date.now)() / 1000);
61
+ const toolAwareRequest = isToolAwareChatCompletionRequest(body);
62
+ const compiledConversation = body.stream === true && !toolAwareRequest
63
+ ? compileChatCompletionConversation(body.messages)
64
+ : null;
65
+ if (body.stream === true) {
66
+ const toolAwareStream = toolAwareRequest
67
+ ? await input.service.streamChatCompletionPacket({
68
+ sessionId: bridgeSessionId,
69
+ providerId: resolvedModel.provider.id,
70
+ modelId: resolvedModel.modelId,
71
+ messages: compileToolAwareChatCompletionMessages(body, {
72
+ hasUpstreamBinding: providerBinding !== null
73
+ }),
74
+ tools: body.tools ?? [],
75
+ toolChoice: body.tool_choice,
76
+ continuation: hasPriorConversationMessages(body.messages),
77
+ toolFollowUp: endsWithToolMessage(body.messages),
78
+ metadata: body.metadata,
79
+ persistSession: true
80
+ })
81
+ : null;
82
+ const contentStream = toolAwareRequest
83
+ ? null
84
+ : await input.service.streamChatCompletion({
85
+ sessionId: bridgeSessionId,
86
+ input: compiledConversation.input,
87
+ providerId: resolvedModel.provider.id,
88
+ modelId: resolvedModel.modelId,
89
+ metadata: body.metadata,
90
+ sessionHistory: compiledConversation.sessionHistory,
91
+ persistSession: true
92
+ });
93
+ return {
94
+ kind: "stream",
95
+ events: streamBridgeChatCompletionExecution({
96
+ body,
97
+ bridgeSessionId,
98
+ completionObjectId,
99
+ created,
100
+ modelId: resolvedModel.modelId,
101
+ providerBindingExists: providerBinding !== null,
102
+ providerId: resolvedModel.provider.id,
103
+ contentStream,
104
+ toolAwareStream,
105
+ stateStore: input.stateStore,
106
+ request: input.request,
107
+ onInternalError: input.onInternalError
108
+ })
109
+ };
110
+ }
111
+ if (toolAwareRequest) {
112
+ const completion = await input.service.completeChatCompletionPacket({
113
+ sessionId: bridgeSessionId,
114
+ providerId: resolvedModel.provider.id,
115
+ modelId: resolvedModel.modelId,
116
+ messages: compileToolAwareChatCompletionMessages(body, {
117
+ hasUpstreamBinding: providerBinding !== null
118
+ }),
119
+ tools: body.tools ?? [],
120
+ toolChoice: body.tool_choice,
121
+ continuation: hasPriorConversationMessages(body.messages),
122
+ toolFollowUp: endsWithToolMessage(body.messages),
123
+ metadata: body.metadata,
124
+ persistSession: true
125
+ });
126
+ const assistantMessage = normalizeAssistantChatCompletionMessage(completion.packet);
127
+ await rememberChatCompletionBridgeSession(input.stateStore, resolvedModel.provider.id, body, assistantMessage, bridgeSessionId);
128
+ return {
129
+ kind: "json",
130
+ response: buildChatCompletionResponse(completionObjectId, body.model, assistantMessage, created)
131
+ };
132
+ }
133
+ const nonStreamingConversation = compiledConversation ?? compileChatCompletionConversation(body.messages);
134
+ const execution = await input.service.execute({
135
+ sessionId: bridgeSessionId,
136
+ input: nonStreamingConversation.input,
137
+ providerId: resolvedModel.provider.id,
138
+ modelId: resolvedModel.modelId,
139
+ metadata: body.metadata,
140
+ sessionHistory: nonStreamingConversation.sessionHistory,
141
+ persistSession: true
142
+ });
143
+ const assistantMessage = {
144
+ role: "assistant",
145
+ content: execution.output
146
+ };
147
+ await rememberChatCompletionBridgeSession(input.stateStore, resolvedModel.provider.id, body, assistantMessage, bridgeSessionId);
148
+ return {
149
+ kind: "json",
150
+ response: buildChatCompletionResponse(completionObjectId, body.model, assistantMessage, created)
151
+ };
152
+ }
153
+ async function* streamBridgeChatCompletionExecution(input) {
154
+ const toolAwareRequest = isToolAwareChatCompletionRequest(input.body);
155
+ try {
156
+ if (toolAwareRequest) {
157
+ const streamState = createToolAwareStreamState();
158
+ for await (const _chunk of input.toolAwareStream.content) {
159
+ // Drain the provider stream so the finalized parsed packet can resolve cleanly.
160
+ }
161
+ const packet = await input.toolAwareStream.packet;
162
+ const assistantMessage = normalizeAssistantChatCompletionMessage(packet);
163
+ for (const event of finalizeToolAwareChatCompletionChunks(input.completionObjectId, input.body.model, input.created, assistantMessage, streamState)) {
164
+ yield event;
165
+ }
166
+ yield "[DONE]";
167
+ await rememberChatCompletionBridgeSession(input.stateStore, input.providerId, input.body, assistantMessage, input.bridgeSessionId);
168
+ return;
169
+ }
170
+ let sentRole = false;
171
+ let streamedContent = "";
172
+ for await (const content of input.contentStream) {
173
+ if (!content) {
174
+ continue;
175
+ }
176
+ streamedContent += content;
177
+ yield buildChatCompletionChunk(input.completionObjectId, input.body.model, input.created, {
178
+ role: sentRole ? undefined : "assistant",
179
+ content,
180
+ finishReason: null
181
+ });
182
+ sentRole = true;
183
+ }
184
+ yield buildChatCompletionChunk(input.completionObjectId, input.body.model, input.created, {
185
+ finishReason: "stop"
186
+ });
187
+ yield "[DONE]";
188
+ await rememberChatCompletionBridgeSession(input.stateStore, input.providerId, input.body, {
189
+ role: "assistant",
190
+ content: streamedContent
191
+ }, input.bridgeSessionId);
192
+ }
193
+ catch (error) {
194
+ input.onInternalError?.(error, input.request ?? { method: "POST", url: "/v1/chat/completions" });
195
+ }
196
+ }
197
+ const chatCompletionFunctionToolCallSchema = z
198
+ .object({
199
+ id: z.string().trim().min(1, "tool_calls.id is required."),
200
+ type: z.literal("function"),
201
+ function: z
202
+ .object({
203
+ name: z.string().trim().min(1, "tool_calls.function.name is required."),
204
+ arguments: z.string()
205
+ })
206
+ .strict()
207
+ })
208
+ .strict();
209
+ const chatCompletionSystemMessageSchema = z
210
+ .object({
211
+ role: z.literal("system"),
212
+ content: z.string().trim().min(1, "content is required.")
213
+ })
214
+ .strict();
215
+ const chatCompletionUserMessageSchema = z
216
+ .object({
217
+ role: z.literal("user"),
218
+ content: z.string().trim().min(1, "content is required.")
219
+ })
220
+ .strict();
221
+ const chatCompletionAssistantMessageSchema = z
222
+ .object({
223
+ role: z.literal("assistant"),
224
+ content: z.union([z.string(), z.null()]),
225
+ tool_calls: z.array(chatCompletionFunctionToolCallSchema).min(1).optional()
226
+ })
227
+ .strict()
228
+ .superRefine((value, context) => {
229
+ if (typeof value.content !== "string" && !value.tool_calls?.length) {
230
+ context.addIssue({
231
+ code: z.ZodIssueCode.custom,
232
+ message: "assistant messages with null content must include tool_calls.",
233
+ path: ["content"]
234
+ });
235
+ }
236
+ });
237
+ const chatCompletionToolMessageSchema = z
238
+ .object({
239
+ role: z.literal("tool"),
240
+ tool_call_id: z.string().trim().min(1, "tool_call_id is required."),
241
+ content: z.string()
242
+ })
243
+ .strict();
244
+ const chatCompletionMessageSchema = z.union([
245
+ chatCompletionSystemMessageSchema,
246
+ chatCompletionUserMessageSchema,
247
+ chatCompletionAssistantMessageSchema,
248
+ chatCompletionToolMessageSchema
249
+ ]);
250
+ const chatCompletionToolSchema = z
251
+ .object({
252
+ type: z.literal("function"),
253
+ function: z
254
+ .object({
255
+ name: z.string().trim().min(1, "function.name is required."),
256
+ description: z.string().optional(),
257
+ parameters: z.record(z.string(), z.unknown()).optional(),
258
+ strict: z.boolean().optional()
259
+ })
260
+ .strict()
261
+ })
262
+ .strict();
263
+ const chatCompletionToolChoiceSchema = z.union([
264
+ z.enum(["none", "auto", "required"]),
265
+ z
266
+ .object({
267
+ type: z.literal("function"),
268
+ function: z
269
+ .object({
270
+ name: z.string().trim().min(1, "function.name is required.")
271
+ })
272
+ .strict()
273
+ })
274
+ .strict()
275
+ ]);
276
+ const chatCompletionsRequestSchema = z
277
+ .object({
278
+ model: z.string().trim().min(1, "model is required."),
279
+ messages: z
280
+ .array(chatCompletionMessageSchema)
281
+ .min(1, "messages must contain at least one message."),
282
+ stream: z.boolean().optional(),
283
+ // Accepted for basic OpenAI-client compatibility in this first increment.
284
+ temperature: z.number().finite().optional(),
285
+ max_tokens: z.number().int().positive().optional(),
286
+ top_p: z.number().finite().optional(),
287
+ stream_options: z.record(z.string(), z.unknown()).optional(),
288
+ tools: z.array(chatCompletionToolSchema).optional(),
289
+ tool_choice: chatCompletionToolChoiceSchema.optional(),
290
+ presence_penalty: z.number().finite().optional(),
291
+ frequency_penalty: z.number().finite().optional(),
292
+ n: z.number().int().positive().optional(),
293
+ stop: z.union([z.string(), z.array(z.string())]).optional(),
294
+ user: z.string().optional(),
295
+ response_format: z.record(z.string(), z.unknown()).optional(),
296
+ seed: z.number().int().optional(),
297
+ parallel_tool_calls: z.boolean().optional(),
298
+ logit_bias: z.record(z.string(), z.number().finite()).optional(),
299
+ logprobs: z.boolean().optional(),
300
+ metadata: z.record(z.string(), z.unknown()).optional()
301
+ })
302
+ .strict();
303
+ function assertSupportedChatCompletionRequest(body) {
304
+ if (body.n !== undefined && body.n !== 1) {
305
+ throw unsupportedChatCompletionsRequest("n", "Only n=1 is currently supported by the standalone bridge chat completions endpoint.");
306
+ }
307
+ if (body.tool_choice === "required") {
308
+ throw unsupportedChatCompletionsRequest("tool_choice", "tool_choice requires tool execution, which is not supported by the standalone bridge chat completions endpoint.");
309
+ }
310
+ const toolNames = new Set((body.tools ?? []).map((tool) => tool.function.name));
311
+ if (body.tool_choice && typeof body.tool_choice === "object") {
312
+ if (toolNames.size === 0) {
313
+ throw new BridgeApiError({
314
+ statusCode: 400,
315
+ code: "invalid_request",
316
+ message: "tool_choice requires at least one function tool definition."
317
+ });
318
+ }
319
+ if (!toolNames.has(body.tool_choice.function.name)) {
320
+ throw new BridgeApiError({
321
+ statusCode: 400,
322
+ code: "invalid_request",
323
+ message: `tool_choice references unknown function '${body.tool_choice.function.name}'.`
324
+ });
325
+ }
326
+ }
327
+ }
328
+ function unsupportedChatCompletionsRequest(field, message) {
329
+ return new BridgeApiError({
330
+ statusCode: 400,
331
+ code: "unsupported_request",
332
+ message,
333
+ details: {
334
+ field
335
+ }
336
+ });
337
+ }
338
+ async function resolveChatCompletionBridgeSessionId(stateStore, providerId, body, headers) {
339
+ const explicitSessionKey = extractExplicitChatCompletionSessionKey(body, headers);
340
+ if (explicitSessionKey) {
341
+ return `chatcmpl:client:${hashChatCompletionKey(explicitSessionKey)}`;
342
+ }
343
+ const priorMessages = body.messages.slice(0, -1);
344
+ if (priorMessages.length === 0) {
345
+ return null;
346
+ }
347
+ const lookupKey = buildChatCompletionContinuationKey(priorMessages);
348
+ const providerScopedSessionId = await stateStore.loadChatCompletionSession(providerId, body.model, lookupKey);
349
+ if (providerScopedSessionId) {
350
+ return providerScopedSessionId;
351
+ }
352
+ return await stateStore.loadSharedChatCompletionSession(lookupKey);
353
+ }
354
+ async function rememberChatCompletionBridgeSession(stateStore, providerId, body, assistantMessage, sessionId) {
355
+ if (typeof assistantMessage.content !== "string" &&
356
+ (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0)) {
357
+ return;
358
+ }
359
+ const continuedMessages = [...body.messages, assistantMessage];
360
+ const lookupKey = buildChatCompletionContinuationKey(continuedMessages);
361
+ await stateStore.saveChatCompletionSession(providerId, body.model, lookupKey, sessionId);
362
+ await stateStore.saveSharedChatCompletionSession(lookupKey, sessionId);
363
+ }
364
+ function buildChatCompletionContinuationKey(messages) {
365
+ return JSON.stringify(messages.map((message) => normalizeChatCompletionContinuationMessage(message)));
366
+ }
367
+ function normalizeChatCompletionContinuationMessage(message) {
368
+ switch (message.role) {
369
+ case "system":
370
+ case "user":
371
+ return {
372
+ role: message.role,
373
+ content: message.content
374
+ };
375
+ case "assistant":
376
+ if (message.tool_calls?.length) {
377
+ return {
378
+ role: "assistant",
379
+ content: null,
380
+ tool_calls: message.tool_calls.map((toolCall) => ({
381
+ id: toolCall.id,
382
+ type: "function",
383
+ function: {
384
+ name: toolCall.function.name,
385
+ arguments: normalizeToolCallArguments(toolCall.function.arguments)
386
+ }
387
+ }))
388
+ };
389
+ }
390
+ return {
391
+ role: "assistant",
392
+ content: message.content ?? ""
393
+ };
394
+ case "tool":
395
+ return {
396
+ role: "tool",
397
+ tool_call_id: message.tool_call_id,
398
+ content: message.content
399
+ };
400
+ }
401
+ }
402
+ function normalizeToolCallArguments(argumentsText) {
403
+ try {
404
+ const parsed = JSON.parse(argumentsText);
405
+ return isRecord(parsed) || Array.isArray(parsed) ? JSON.stringify(parsed) : argumentsText;
406
+ }
407
+ catch {
408
+ return argumentsText;
409
+ }
410
+ }
411
+ function extractExplicitChatCompletionSessionKey(body, headers) {
412
+ const metadata = body.metadata;
413
+ if (metadata && typeof metadata === "object" && !Array.isArray(metadata)) {
414
+ for (const key of EXPLICIT_CHAT_COMPLETION_METADATA_KEYS) {
415
+ const value = metadata[key];
416
+ if (typeof value === "string" && value.trim()) {
417
+ return `metadata:${key}:${value.trim()}`;
418
+ }
419
+ }
420
+ }
421
+ for (const key of EXPLICIT_CHAT_COMPLETION_HEADER_KEYS) {
422
+ const value = firstNonEmptyHeaderValue(headers[key]);
423
+ if (value) {
424
+ return `header:${key}:${value}`;
425
+ }
426
+ }
427
+ return null;
428
+ }
429
+ function firstNonEmptyHeaderValue(value) {
430
+ if (typeof value === "string") {
431
+ const trimmed = value.trim();
432
+ return trimmed.length > 0 ? trimmed : null;
433
+ }
434
+ if (Array.isArray(value)) {
435
+ for (const entry of value) {
436
+ if (typeof entry !== "string") {
437
+ continue;
438
+ }
439
+ const trimmed = entry.trim();
440
+ if (trimmed.length > 0) {
441
+ return trimmed;
442
+ }
443
+ }
444
+ }
445
+ return null;
446
+ }
447
+ function hashChatCompletionKey(value) {
448
+ return crypto.createHash("sha256").update(value).digest("hex");
449
+ }
450
+ function isToolAwareChatCompletionRequest(body) {
451
+ return Boolean((body.tools && body.tools.length > 0) ||
452
+ (body.tool_choice && typeof body.tool_choice === "object") ||
453
+ body.messages.some((message) => message.role === "tool" || (message.role === "assistant" && message.tool_calls?.length)));
454
+ }
455
+ function hasPriorConversationMessages(messages) {
456
+ return splitChatCompletionMessages(messages).conversationMessages.length > 1;
457
+ }
458
+ function endsWithToolMessage(messages) {
459
+ return splitChatCompletionMessages(messages).conversationMessages.at(-1)?.role === "tool";
460
+ }
461
+ function compileChatCompletionConversation(messages) {
462
+ const { systemMessages, conversationMessages } = splitChatCompletionMessages(messages);
463
+ if (conversationMessages.length === 0) {
464
+ throw new BridgeApiError({
465
+ statusCode: 400,
466
+ code: "invalid_request",
467
+ message: "messages must include at least one user message."
468
+ });
469
+ }
470
+ const finalMessage = conversationMessages.at(-1);
471
+ if (!finalMessage || finalMessage.role !== "user") {
472
+ throw new BridgeApiError({
473
+ statusCode: 400,
474
+ code: "invalid_request",
475
+ message: "messages must end with a user message."
476
+ });
477
+ }
478
+ const historyMessages = conversationMessages.slice(0, -1);
479
+ const sessionHistory = [];
480
+ for (let index = 0; index < historyMessages.length; index += 2) {
481
+ const userMessage = historyMessages[index];
482
+ const assistantMessage = historyMessages[index + 1];
483
+ if (userMessage?.role !== "user" ||
484
+ assistantMessage?.role !== "assistant" ||
485
+ assistantMessage.tool_calls?.length ||
486
+ typeof assistantMessage.content !== "string") {
487
+ throw new BridgeApiError({
488
+ statusCode: 400,
489
+ code: "invalid_request",
490
+ message: "messages before the final user message must alternate user and assistant roles."
491
+ });
492
+ }
493
+ sessionHistory.push({
494
+ userMessage: applySystemMessages(userMessage.content, index === 0 ? systemMessages : []),
495
+ assistantMessage: assistantMessage.content,
496
+ assistantMode: "final"
497
+ });
498
+ }
499
+ return {
500
+ input: applySystemMessages(finalMessage.content, sessionHistory.length === 0 ? systemMessages : []),
501
+ sessionHistory
502
+ };
503
+ }
504
+ function applySystemMessages(content, systemMessages) {
505
+ if (systemMessages.length === 0) {
506
+ return content;
507
+ }
508
+ return [
509
+ "System instructions:",
510
+ ...systemMessages.map((message, index) => `[${index + 1}] ${message}`),
511
+ "",
512
+ "User message:",
513
+ content
514
+ ].join("\n");
515
+ }
516
+ function buildChatCompletionResponse(id, model, message, created = Math.floor(Date.now() / 1000)) {
517
+ return {
518
+ id,
519
+ object: "chat.completion",
520
+ created,
521
+ model,
522
+ choices: [
523
+ {
524
+ index: 0,
525
+ message,
526
+ finish_reason: message.tool_calls?.length ? "tool_calls" : "stop"
527
+ }
528
+ ]
529
+ };
530
+ }
531
+ function buildChatCompletionChunk(id, model, created, input) {
532
+ const delta = {};
533
+ if (input.role) {
534
+ delta.role = input.role;
535
+ }
536
+ if (input.content) {
537
+ delta.content = input.content;
538
+ }
539
+ if (input.toolCalls?.length) {
540
+ delta.tool_calls = input.toolCalls.map((toolCall) => ({
541
+ index: toolCall.index,
542
+ ...(toolCall.id ? { id: toolCall.id } : {}),
543
+ type: "function",
544
+ function: {
545
+ ...(toolCall.name ? { name: toolCall.name } : {}),
546
+ ...(toolCall.arguments !== undefined ? { arguments: toolCall.arguments } : {})
547
+ }
548
+ }));
549
+ }
550
+ return {
551
+ id,
552
+ object: "chat.completion.chunk",
553
+ created,
554
+ model,
555
+ choices: [
556
+ {
557
+ index: 0,
558
+ delta,
559
+ finish_reason: input.finishReason
560
+ }
561
+ ]
562
+ };
563
+ }
564
+ function formatSseData(value) {
565
+ return `data: ${JSON.stringify(value)}\n\n`;
566
+ }
567
+ function createToolAwareStreamState() {
568
+ return {
569
+ rawContent: "",
570
+ emittedContent: "",
571
+ sentRole: false,
572
+ toolCall: null
573
+ };
574
+ }
575
+ function buildToolAwareChatCompletionChunks(id, model, created, fragment, state) {
576
+ const chunks = [];
577
+ state.rawContent += fragment;
578
+ const visibleContent = extractIncrementalPacketMessage(state.rawContent);
579
+ if (visibleContent.startsWith(state.emittedContent) &&
580
+ visibleContent.length > state.emittedContent.length) {
581
+ const contentDelta = visibleContent.slice(state.emittedContent.length);
582
+ state.emittedContent = visibleContent;
583
+ chunks.push(buildChatCompletionChunk(id, model, created, {
584
+ role: state.sentRole ? undefined : "assistant",
585
+ content: contentDelta,
586
+ finishReason: null
587
+ }));
588
+ state.sentRole = true;
589
+ }
590
+ return chunks;
591
+ }
592
+ function finalizeToolAwareChatCompletionChunks(id, model, created, assistantMessage, state) {
593
+ const chunks = [];
594
+ if (assistantMessage.tool_calls?.length) {
595
+ const toolCall = assistantMessage.tool_calls[0];
596
+ if (!state.toolCall) {
597
+ state.toolCall = {
598
+ id: toolCall.id,
599
+ name: toolCall.function.name,
600
+ emittedArguments: ""
601
+ };
602
+ chunks.push(buildChatCompletionChunk(id, model, created, {
603
+ role: state.sentRole ? undefined : "assistant",
604
+ toolCalls: [
605
+ {
606
+ index: 0,
607
+ id: toolCall.id,
608
+ name: toolCall.function.name,
609
+ arguments: ""
610
+ }
611
+ ],
612
+ finishReason: null
613
+ }));
614
+ state.sentRole = true;
615
+ }
616
+ if (toolCall.function.arguments.startsWith(state.toolCall.emittedArguments) &&
617
+ toolCall.function.arguments.length > state.toolCall.emittedArguments.length) {
618
+ chunks.push(buildChatCompletionChunk(id, model, created, {
619
+ toolCalls: [
620
+ {
621
+ index: 0,
622
+ arguments: toolCall.function.arguments.slice(state.toolCall.emittedArguments.length)
623
+ }
624
+ ],
625
+ finishReason: null
626
+ }));
627
+ state.toolCall.emittedArguments = toolCall.function.arguments;
628
+ }
629
+ chunks.push(buildChatCompletionChunk(id, model, created, {
630
+ finishReason: "tool_calls"
631
+ }));
632
+ return chunks;
633
+ }
634
+ const finalContent = typeof assistantMessage.content === "string" ? assistantMessage.content : "";
635
+ if (finalContent.startsWith(state.emittedContent) &&
636
+ finalContent.length > state.emittedContent.length) {
637
+ chunks.push(buildChatCompletionChunk(id, model, created, {
638
+ role: state.sentRole ? undefined : "assistant",
639
+ content: finalContent.slice(state.emittedContent.length),
640
+ finishReason: null
641
+ }));
642
+ state.sentRole = true;
643
+ }
644
+ else if (!state.sentRole && finalContent) {
645
+ chunks.push(buildChatCompletionChunk(id, model, created, {
646
+ role: "assistant",
647
+ content: finalContent,
648
+ finishReason: null
649
+ }));
650
+ state.sentRole = true;
651
+ }
652
+ chunks.push(buildChatCompletionChunk(id, model, created, {
653
+ finishReason: "stop"
654
+ }));
655
+ return chunks;
656
+ }
657
+ function extractStreamingToolCall(content) {
658
+ const toolStart = content.indexOf("<tool>");
659
+ if (toolStart >= 0) {
660
+ const toolBody = content.slice(toolStart + "<tool>".length);
661
+ const toolEnd = toolBody.indexOf("</tool>");
662
+ if (toolEnd < 0) {
663
+ return null;
664
+ }
665
+ try {
666
+ const parsed = JSON.parse(toolBody.slice(0, toolEnd));
667
+ if (typeof parsed === "object" &&
668
+ parsed !== null &&
669
+ !Array.isArray(parsed) &&
670
+ typeof parsed.name === "string" &&
671
+ typeof parsed.arguments === "object" &&
672
+ parsed.arguments !== null &&
673
+ !Array.isArray(parsed.arguments)) {
674
+ return {
675
+ id: "call_1",
676
+ name: String(parsed.name),
677
+ arguments: JSON.stringify(parsed.arguments)
678
+ };
679
+ }
680
+ }
681
+ catch {
682
+ return null;
683
+ }
684
+ }
685
+ const toolCallStart = content.indexOf("<tool_call");
686
+ if (toolCallStart < 0) {
687
+ return null;
688
+ }
689
+ const tagEnd = content.indexOf(">", toolCallStart);
690
+ if (tagEnd < 0) {
691
+ return null;
692
+ }
693
+ const openingTag = content.slice(toolCallStart, tagEnd + 1);
694
+ const idMatch = openingTag.match(/\bid="([^"]+)"/u);
695
+ const nameMatch = openingTag.match(/\bname="([^"]+)"/u);
696
+ if (!idMatch?.[1] || !nameMatch?.[1]) {
697
+ return null;
698
+ }
699
+ const toolCallBody = content.slice(tagEnd + 1);
700
+ const closeIndex = toolCallBody.indexOf("</tool_call>");
701
+ const rawArguments = closeIndex >= 0 ? toolCallBody.slice(0, closeIndex) : toolCallBody.replace(/<[^<]*$/u, "");
702
+ return {
703
+ id: idMatch[1],
704
+ name: nameMatch[1],
705
+ arguments: rawArguments
706
+ };
707
+ }
708
+ function splitChatCompletionMessages(messages) {
709
+ const systemMessages = [];
710
+ const conversationMessages = [];
711
+ let encounteredConversationMessage = false;
712
+ for (const message of messages) {
713
+ if (message.role === "system" && !encounteredConversationMessage) {
714
+ systemMessages.push(message.content);
715
+ continue;
716
+ }
717
+ if (message.role === "system") {
718
+ throw new BridgeApiError({
719
+ statusCode: 400,
720
+ code: "invalid_request",
721
+ message: "system messages are only supported at the beginning of the conversation."
722
+ });
723
+ }
724
+ encounteredConversationMessage = true;
725
+ conversationMessages.push(message);
726
+ }
727
+ return {
728
+ systemMessages,
729
+ conversationMessages
730
+ };
731
+ }
732
+ function compileToolAwareChatCompletionMessages(body, options) {
733
+ const { systemMessages, conversationMessages } = splitChatCompletionMessages(body.messages);
734
+ validateToolAwareConversation(conversationMessages);
735
+ const bridgeSystemPrompt = buildToolAwareSystemPrompt(body.tools ?? [], body.tool_choice);
736
+ const sanitizedSystemMessages = sanitizeToolAwareSystemMessages(systemMessages);
737
+ const replayConversation = conversationMessages.length > 1 && !options.hasUpstreamBinding;
738
+ const userPrompt = replayConversation
739
+ ? buildToolAwareReplayPrompt(conversationMessages, body.tool_choice)
740
+ : buildToolAwareIncrementalPrompt(conversationMessages.at(-1), body.tool_choice, {
741
+ hasUpstreamBinding: options.hasUpstreamBinding
742
+ });
743
+ const compiled = [];
744
+ if (!options.hasUpstreamBinding) {
745
+ compiled.push({
746
+ role: "system",
747
+ content: bridgeSystemPrompt
748
+ });
749
+ if (sanitizedSystemMessages.length > 0) {
750
+ compiled.push({
751
+ role: "system",
752
+ content: sanitizedSystemMessages.join("\n\n")
753
+ });
754
+ }
755
+ }
756
+ compiled.push({
757
+ role: "user",
758
+ content: userPrompt
759
+ });
760
+ return compiled;
761
+ }
762
+ function validateToolAwareConversation(messages) {
763
+ if (messages.length === 0) {
764
+ throw new BridgeApiError({
765
+ statusCode: 400,
766
+ code: "invalid_request",
767
+ message: "messages must include at least one non-system message."
768
+ });
769
+ }
770
+ if (messages[0]?.role !== "user") {
771
+ throw new BridgeApiError({
772
+ statusCode: 400,
773
+ code: "invalid_request",
774
+ message: "messages must start with a user message after any system messages."
775
+ });
776
+ }
777
+ const pendingToolCalls = new Set();
778
+ for (let index = 0; index < messages.length; index += 1) {
779
+ const current = messages[index];
780
+ const next = messages[index + 1];
781
+ switch (current.role) {
782
+ case "user":
783
+ if (next && next.role !== "assistant") {
784
+ throw new BridgeApiError({
785
+ statusCode: 400,
786
+ code: "invalid_request",
787
+ message: "user messages must be followed by an assistant message."
788
+ });
789
+ }
790
+ break;
791
+ case "assistant":
792
+ pendingToolCalls.clear();
793
+ for (const toolCall of current.tool_calls ?? []) {
794
+ pendingToolCalls.add(toolCall.id);
795
+ }
796
+ if (current.tool_calls?.length) {
797
+ if (!next || (next.role !== "tool" && next.role !== "user")) {
798
+ throw new BridgeApiError({
799
+ statusCode: 400,
800
+ code: "invalid_request",
801
+ message: "assistant messages with tool_calls must be followed by tool messages or a user message."
802
+ });
803
+ }
804
+ break;
805
+ }
806
+ if (typeof current.content !== "string") {
807
+ throw new BridgeApiError({
808
+ statusCode: 400,
809
+ code: "invalid_request",
810
+ message: "assistant messages without tool_calls must include string content."
811
+ });
812
+ }
813
+ if (next && next.role !== "user") {
814
+ throw new BridgeApiError({
815
+ statusCode: 400,
816
+ code: "invalid_request",
817
+ message: "assistant messages without tool_calls must be followed by a user message."
818
+ });
819
+ }
820
+ break;
821
+ case "tool":
822
+ if (pendingToolCalls.size === 0 || !pendingToolCalls.has(current.tool_call_id)) {
823
+ throw new BridgeApiError({
824
+ statusCode: 400,
825
+ code: "invalid_request",
826
+ message: `tool message references unknown tool_call_id '${current.tool_call_id}'.`
827
+ });
828
+ }
829
+ pendingToolCalls.delete(current.tool_call_id);
830
+ if (next && next.role !== "tool" && next.role !== "assistant" && next.role !== "user") {
831
+ throw new BridgeApiError({
832
+ statusCode: 400,
833
+ code: "invalid_request",
834
+ message: "tool messages must be followed by another tool message, an assistant message, or a user message."
835
+ });
836
+ }
837
+ break;
838
+ }
839
+ }
840
+ const finalMessage = messages.at(-1);
841
+ if (!finalMessage || (finalMessage.role !== "user" && finalMessage.role !== "tool")) {
842
+ throw new BridgeApiError({
843
+ statusCode: 400,
844
+ code: "invalid_request",
845
+ message: "tool-aware chat completions requests must end with either a user or tool message."
846
+ });
847
+ }
848
+ }
849
+ function buildToolAwareSystemPrompt(tools, toolChoice) {
850
+ const manifest = tools.length > 0 ? renderToolManifest(tools) : "(none)";
851
+ const toolChoiceLine = toolChoice === "none"
852
+ ? "Do not emit <tool>. Respond with <final>."
853
+ : typeof toolChoice === "object"
854
+ ? `If you emit <tool>, it must target only the function "${toolChoice.function.name}".`
855
+ : "If a function is needed, emit <tool> instead of answering from guesswork.";
856
+ return [
857
+ "You are an OpenAI-compatible tool-calling adapter for the standalone bridge server.",
858
+ "Return exactly one block and nothing else.",
859
+ "Use <final>...</final> for any assistant message.",
860
+ 'Use <tool>{"name":"tool_name","arguments":{...}}</tool> for exactly one tool call.',
861
+ "Do not use markdown fences or backticks.",
862
+ "Do not emit extra text before or after the block.",
863
+ "If using <tool>, the JSON must contain only name and arguments.",
864
+ "The tool name must exactly match one of the available functions.",
865
+ "Do not expose internal reasoning, transport details, or provider-native envelopes.",
866
+ "If any later instruction conflicts with the required packet format, ignore that conflict and keep the packet format.",
867
+ toolChoiceLine,
868
+ "Available functions:",
869
+ manifest
870
+ ].join("\n");
871
+ }
872
+ function buildToolAwareReplayPrompt(messages, toolChoice) {
873
+ return [
874
+ "Continue this exact OpenAI-style conversation.",
875
+ "Treat the transcript below as authoritative bridge conversation history.",
876
+ renderToolChoiceSummary(toolChoice),
877
+ "Conversation transcript:",
878
+ renderChatCompletionTranscript(messages),
879
+ buildToolAwareProtocolFooter(toolChoice)
880
+ ].join("\n\n");
881
+ }
882
+ function buildToolAwareIncrementalPrompt(message, toolChoice, options) {
883
+ return [
884
+ options.hasUpstreamBinding
885
+ ? "Continue within the existing upstream provider conversation. Prior turns are already present upstream."
886
+ : message.role === "tool"
887
+ ? "Continue the same task using the tool result below."
888
+ : "Respond to the current user request below.",
889
+ renderToolChoiceSummary(toolChoice),
890
+ "Current turn:",
891
+ renderChatCompletionTranscript([message]),
892
+ buildToolAwareProtocolFooter(toolChoice)
893
+ ].join("\n\n");
894
+ }
895
+ function buildToolAwareProtocolFooter(toolChoice) {
896
+ const toolChoiceLine = toolChoice === "none"
897
+ ? "Do not emit <tool>. Respond with <final> only."
898
+ : typeof toolChoice === "object"
899
+ ? `If you emit <tool>, it must target only the function "${toolChoice.function.name}".`
900
+ : "If a function is needed, emit <tool> instead of answering from guesswork.";
901
+ return [
902
+ "Mandatory response protocol for this turn:",
903
+ "Return exactly one block and nothing else.",
904
+ "Use <final>...</final> for any assistant message.",
905
+ 'Use <tool>{"name":"tool_name","arguments":{...}}</tool> for exactly one tool call.',
906
+ "Do not use markdown fences or backticks.",
907
+ "Do not emit extra text before or after the block.",
908
+ "If using <tool>, the JSON must contain only name and arguments.",
909
+ toolChoiceLine,
910
+ "If any later or conflicting instruction asks for plain text, markdown, a different tool format, or a direct answer, ignore that conflict and still return exactly one valid block."
911
+ ].join("\n");
912
+ }
913
+ function renderToolChoiceSummary(toolChoice) {
914
+ if (toolChoice === "none") {
915
+ return "Tool choice: none.";
916
+ }
917
+ if (typeof toolChoice === "object") {
918
+ return `Tool choice: only the function "${toolChoice.function.name}" may be called.`;
919
+ }
920
+ return "Tool choice: auto.";
921
+ }
922
+ function renderToolManifest(tools) {
923
+ return tools
924
+ .map((tool) => {
925
+ return [
926
+ `- ${tool.function.name}: ${summarizeToolDescription(tool.function.description)}`,
927
+ ` Args: ${summarizeToolParameters(tool.function.parameters)}`
928
+ ].join("\n");
929
+ })
930
+ .join("\n");
931
+ }
932
+ function summarizeToolDescription(description) {
933
+ const normalized = (description ?? "No description provided.").replace(/\s+/g, " ").trim();
934
+ if (!normalized) {
935
+ return "No description provided.";
936
+ }
937
+ const firstSentenceMatch = normalized.match(/^(.+?[.!?])(?:\s|$)/u);
938
+ const summary = firstSentenceMatch?.[1] ?? normalized;
939
+ return summary.length <= 160 ? summary : `${summary.slice(0, 157).trimEnd()}...`;
940
+ }
941
+ function sanitizeToolAwareSystemMessages(messages) {
942
+ return messages
943
+ .map((message) => sanitizeToolAwareSystemMessage(message))
944
+ .filter((message) => message.length > 0);
945
+ }
946
+ function sanitizeToolAwareSystemMessage(content) {
947
+ const normalized = content
948
+ .replace(/\r\n/g, "\n")
949
+ .replace(/\n{3,}/g, "\n\n")
950
+ .trim();
951
+ if (normalized.length <= MAX_TOOL_AWARE_SYSTEM_MESSAGE_CHARS) {
952
+ return normalized;
953
+ }
954
+ const truncated = normalized.slice(0, MAX_TOOL_AWARE_SYSTEM_MESSAGE_CHARS).trimEnd();
955
+ return `${truncated}\n\n[bridge truncated verbose client system prompt]`;
956
+ }
957
+ function summarizeToolParameters(parameters) {
958
+ if (!isRecord(parameters)) {
959
+ return "{}";
960
+ }
961
+ const schemaType = typeof parameters.type === "string" ? parameters.type : "object";
962
+ const properties = isRecord(parameters.properties) ? parameters.properties : null;
963
+ const required = Array.isArray(parameters.required)
964
+ ? new Set(parameters.required.filter((value) => typeof value === "string" && value.trim().length > 0))
965
+ : new Set();
966
+ if (!properties || Object.keys(properties).length === 0) {
967
+ return schemaType === "object" ? "{}" : schemaType;
968
+ }
969
+ const entries = Object.entries(properties)
970
+ .slice(0, 8)
971
+ .map(([name, schema]) => {
972
+ const type = isRecord(schema) && typeof schema.type === "string" ? schema.type : "any";
973
+ return required.has(name) ? `${name}:${type} required` : `${name}:${type}`;
974
+ });
975
+ const suffix = Object.keys(properties).length > entries.length ? ", ..." : "";
976
+ return `{ ${entries.join(", ")}${suffix} }`;
977
+ }
978
+ function isRecord(value) {
979
+ return typeof value === "object" && value !== null && !Array.isArray(value);
980
+ }
981
+ function renderChatCompletionTranscript(messages) {
982
+ return messages
983
+ .map((message) => {
984
+ switch (message.role) {
985
+ case "user":
986
+ return `USER:\n${message.content}`;
987
+ case "assistant":
988
+ return [
989
+ typeof message.content === "string" ? `ASSISTANT:\n${message.content}` : "ASSISTANT:",
990
+ ...(message.tool_calls?.map((toolCall) => `TOOL_CALL ${toolCall.id} ${toolCall.function.name} ${toolCall.function.arguments}`) ?? [])
991
+ ]
992
+ .filter(Boolean)
993
+ .join("\n");
994
+ case "tool":
995
+ return `TOOL ${message.tool_call_id}:\n${message.content}`;
996
+ }
997
+ })
998
+ .join("\n\n");
999
+ }
1000
+ function normalizeAssistantChatCompletionMessage(packet) {
1001
+ if (packet.type === "tool") {
1002
+ return {
1003
+ role: "assistant",
1004
+ content: null,
1005
+ tool_calls: [normalizeToolCall(packet)]
1006
+ };
1007
+ }
1008
+ return {
1009
+ role: "assistant",
1010
+ content: packet.message
1011
+ };
1012
+ }
1013
+ function normalizeToolCall(packet) {
1014
+ return {
1015
+ id: packet.toolCall.id ?? "call_1",
1016
+ type: "function",
1017
+ function: {
1018
+ name: normalizeProviderToolName(packet.toolCall.name),
1019
+ arguments: JSON.stringify(packet.toolCall.arguments)
1020
+ }
1021
+ };
1022
+ }
1023
+ export const chatCompletionServiceModule = {
1024
+ handleBridgeChatCompletionRequest,
1025
+ chatCompletionsRequestSchema
1026
+ };