@uncensoredcode/openbridge 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uncensoredcode/openbridge",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "OpenBridge runtime, server, and CLI for bridging AI providers through a unified interface.",
5
5
  "packageManager": "bun@1.3.11",
6
6
  "type": "module",
@@ -1,5 +1,16 @@
1
1
  const DEFAULT_BASE_URL = "http://127.0.0.1:4318";
2
- const SERVER_SUBCOMMANDS = new Set(["start", "chat", "live-canary", "clear-session-vault"]);
2
+ const SERVER_SUBCOMMANDS = new Set([
3
+ "start",
4
+ "status",
5
+ "stop",
6
+ "logs",
7
+ "chat",
8
+ "live-canary",
9
+ "clear-session-vault",
10
+ "providers",
11
+ "models",
12
+ "sessions"
13
+ ]);
3
14
  function parseBridgeCliArgs(input) {
4
15
  const env = input.env ?? process.env;
5
16
  const args = [...input.argv];
@@ -55,8 +66,14 @@ function getBridgeCliHelpText() {
55
66
  "openbridge",
56
67
  "",
57
68
  "Usage:",
58
- " openbridge start [--host <host>] [--port <port>] [--token <token>]",
69
+ " openbridge start [--host <host>] [--port <port>] [--token <token>] [--foreground]",
70
+ " openbridge status [--state-root <path>]",
71
+ " openbridge stop [--state-root <path>]",
72
+ " openbridge logs [--follow] [--lines <count>] [--state-root <path>]",
59
73
  " openbridge health [--base-url <url>]",
74
+ " openbridge providers <list|get|add|remove|enable|disable|import-session|session-status|clear-session> ...",
75
+ " openbridge models <list|add> ...",
76
+ " openbridge sessions <list|get|remove> ...",
60
77
  " openbridge --session <id> [--input <text>] [--base-url <url>] [--provider <id>] [--model <id>] [--metadata <json>]",
61
78
  " openbridge chat --model <id> --message <text> [--system <text>] [--base-url <url>] [--stream]",
62
79
  " openbridge live-canary [--state-root <path>] [--provider <id>] [--model <id>]",
@@ -64,7 +81,11 @@ function getBridgeCliHelpText() {
64
81
  "",
65
82
  "Examples:",
66
83
  " openbridge start",
84
+ " openbridge status",
85
+ " openbridge logs --follow",
67
86
  " openbridge health",
87
+ " openbridge providers list",
88
+ " openbridge providers import-session provider-a --file ./session-package.json",
68
89
  ' openbridge --session demo "Read README.md"',
69
90
  ' openbridge --base-url http://127.0.0.1:4318 --session s1 --input "Run git status"'
70
91
  ].join("\n");
@@ -1,5 +1,6 @@
1
1
  const PROVIDER_TOOL_NAME_ALIASES = {
2
- execute_shell_command: "bash"
2
+ execute_shell_command: "bash",
3
+ code_interpreter: "bash"
3
4
  };
4
5
  function normalizeProviderToolName(name) {
5
6
  const trimmed = name.trim();
@@ -36,7 +36,7 @@ const EXPLICIT_CHAT_COMPLETION_HEADER_KEYS = [
36
36
  "x-thread-id"
37
37
  ];
38
38
  async function handleBridgeChatCompletionRequest(input) {
39
- const body = input.body;
39
+ const body = normalizeRecoverableChatCompletionRequest(input.body);
40
40
  assertSupportedChatCompletionRequest(body);
41
41
  const resolvedModel = resolveBridgeModel(input.providerStore.list(), body.model);
42
42
  if (!resolvedModel) {
@@ -300,6 +300,29 @@ const chatCompletionsRequestSchema = z
300
300
  metadata: z.record(z.string(), z.unknown()).optional()
301
301
  })
302
302
  .strict();
303
+ function normalizeRecoverableChatCompletionRequest(body) {
304
+ const messages = normalizeRecoverableChatCompletionMessages(body.messages);
305
+ return messages === body.messages
306
+ ? body
307
+ : {
308
+ ...body,
309
+ messages
310
+ };
311
+ }
312
+ function normalizeRecoverableChatCompletionMessages(messages) {
313
+ const normalized = [];
314
+ let changed = false;
315
+ for (const message of messages) {
316
+ const previous = normalized.at(-1);
317
+ if (message.role === "user" && previous?.role === "user") {
318
+ normalized[normalized.length - 1] = message;
319
+ changed = true;
320
+ continue;
321
+ }
322
+ normalized.push(message);
323
+ }
324
+ return changed ? normalized : messages;
325
+ }
303
326
  function assertSupportedChatCompletionRequest(body) {
304
327
  if (body.n !== undefined && body.n !== 1) {
305
328
  throw unsupportedChatCompletionsRequest("n", "Only n=1 is currently supported by the standalone bridge chat completions endpoint.");
@@ -5,7 +5,7 @@ import { z } from "zod";
5
5
  import { bridgeApiErrorModule } from "../../shared/bridge-api-error.js";
6
6
  import { providerStoreModule } from "./provider-store.js";
7
7
  import { sessionPackageStoreModule } from "./session-package-store.js";
8
- const { buildSessionPackageMetadata, cloneConfig, cloneInstalledProviderPackage, cloneProvider, cloneSessionPackage, cloneSessionPackageMetadata, inferProviderFromSessionPackage, installedProviderPackageSchema, sessionPackageDeleteResponseSchema, sessionPackageSchema, sessionPackageMetadataSchema } = sessionPackageStoreModule;
8
+ const { buildQwenConversationRequestBody, buildSessionPackageMetadata, cloneConfig, cloneInstalledProviderPackage, cloneProvider, cloneSessionPackage, cloneSessionPackageMetadata, inferProviderFromSessionPackage, installedProviderPackageSchema, sessionPackageDeleteResponseSchema, sessionPackageSchema, sessionPackageMetadataSchema } = sessionPackageStoreModule;
9
9
  const { createProviderRequestSchema, providerDeleteResponseSchema, providerSchema } = providerStoreModule;
10
10
  const { BridgeApiError } = bridgeApiErrorModule;
11
11
  const SESSION_VAULT_VERSION = 1;
@@ -315,7 +315,7 @@ function createLocalSessionPackageStore(options) {
315
315
  return null;
316
316
  }
317
317
  const entry = readVaultEntry(metadata);
318
- return decryptInstalledPackage(entry, key);
318
+ return normalizeInstalledProviderPackage(decryptInstalledPackage(entry, key));
319
319
  }
320
320
  function readVaultIndex() {
321
321
  if (!existsSync(indexPath)) {
@@ -372,6 +372,53 @@ function createLocalSessionPackageStore(options) {
372
372
  return path.join(entriesPath, `${handle}.json`);
373
373
  }
374
374
  }
375
+ function normalizeInstalledProviderPackage(installed) {
376
+ const transport = readInstalledQwenTransport(installed.provider.config);
377
+ if (!transport) {
378
+ return installed;
379
+ }
380
+ const request = readInstalledTransportRequest(transport);
381
+ if (!request || !("body" in request)) {
382
+ return installed;
383
+ }
384
+ const normalizedBody = buildQwenConversationRequestBody(request.body);
385
+ if (JSON.stringify(normalizedBody) === JSON.stringify(request.body)) {
386
+ return installed;
387
+ }
388
+ return installedProviderPackageSchema.parse({
389
+ provider: {
390
+ ...cloneProvider(installed.provider),
391
+ config: {
392
+ ...cloneConfig(installed.provider.config),
393
+ transport: {
394
+ ...transport,
395
+ request: {
396
+ ...request,
397
+ body: normalizedBody
398
+ }
399
+ }
400
+ }
401
+ },
402
+ session: installed.session ? cloneSessionPackage(installed.session) : null
403
+ });
404
+ }
405
+ function readInstalledQwenTransport(config) {
406
+ const transport = typeof config.transport === "object" &&
407
+ config.transport !== null &&
408
+ !Array.isArray(config.transport)
409
+ ? config.transport
410
+ : null;
411
+ const request = readInstalledTransportRequest(transport);
412
+ const url = typeof request?.url === "string" ? request.url.trim() : "";
413
+ return /^https:\/\/chat\.qwen\.ai\/api\/v2\/chat\/completions\b/u.test(url) ? transport : null;
414
+ }
415
+ function readInstalledTransportRequest(transport) {
416
+ return typeof transport?.request === "object" &&
417
+ transport.request !== null &&
418
+ !Array.isArray(transport.request)
419
+ ? transport.request
420
+ : null;
421
+ }
375
422
  function encryptInstalledPackage(metadata, value, key) {
376
423
  const iv = crypto.randomBytes(12);
377
424
  const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
@@ -165,6 +165,7 @@ declare function inferProviderFromSessionPackage(input: {
165
165
  createdAt: string;
166
166
  updatedAt: string;
167
167
  };
168
+ declare function buildQwenConversationRequestBody(requestBody: unknown): unknown;
168
169
  export declare const sessionPackageStoreModule: {
169
170
  sessionPackageSchema: z.ZodObject<{
170
171
  schemaVersion: z.ZodOptional<z.ZodNumber>;
@@ -274,6 +275,7 @@ export declare const sessionPackageStoreModule: {
274
275
  }, z.core.$strict>>;
275
276
  }, z.core.$strict>;
276
277
  createInMemorySessionPackageStore: typeof createInMemorySessionPackageStore;
278
+ buildQwenConversationRequestBody: typeof buildQwenConversationRequestBody;
277
279
  buildSessionPackageStatus: typeof buildSessionPackageStatus;
278
280
  cloneProvider: typeof cloneProvider;
279
281
  cloneConfig: typeof cloneConfig;
@@ -416,7 +416,9 @@ function inferQwenConversationTransport(selectedRequest, value) {
416
416
  method: String(selectedRequest.method).toUpperCase(),
417
417
  url: "https://chat.qwen.ai/api/v2/chat/completions?chat_id={{conversationId}}",
418
418
  headers: {},
419
- ...(requestBody === undefined ? {} : { body: requestBody })
419
+ ...(requestBody === undefined
420
+ ? {}
421
+ : { body: buildQwenConversationRequestBody(requestBody) })
420
422
  },
421
423
  response: {
422
424
  contentPaths: DEFAULT_SSE_CONTENT_PATHS,
@@ -926,6 +928,60 @@ function buildQwenBootstrapRequestBody(selectedRequest) {
926
928
  project_id: projectId
927
929
  };
928
930
  }
931
+ function buildQwenConversationRequestBody(requestBody) {
932
+ if (typeof requestBody !== "object" || requestBody === null || Array.isArray(requestBody)) {
933
+ return requestBody;
934
+ }
935
+ const base = structuredClone(requestBody);
936
+ if ("chat_id" in base) {
937
+ base.chat_id = "{{conversationId}}";
938
+ }
939
+ if ("model" in base) {
940
+ base.model = "{{modelId}}";
941
+ }
942
+ if ("timestamp" in base) {
943
+ base.timestamp = "{{unixTimestampSec}}";
944
+ }
945
+ if ("parent_id" in base) {
946
+ base.parent_id = "{{parentIdOrNull}}";
947
+ }
948
+ if (Array.isArray(base.messages) && base.messages.length > 0) {
949
+ base.messages = base.messages.map((message, index) => index === 0 ? buildQwenConversationUserMessage(message) : message);
950
+ }
951
+ return base;
952
+ }
953
+ function buildQwenConversationUserMessage(message) {
954
+ if (typeof message !== "object" || message === null || Array.isArray(message)) {
955
+ return {
956
+ fid: "{{messageId}}",
957
+ role: "user",
958
+ content: "{{prompt}}"
959
+ };
960
+ }
961
+ const nextMessage = structuredClone(message);
962
+ nextMessage.fid = "{{messageId}}";
963
+ nextMessage.role = "user";
964
+ nextMessage.content = "{{prompt}}";
965
+ if ("timestamp" in nextMessage) {
966
+ nextMessage.timestamp = "{{unixTimestampSec}}";
967
+ }
968
+ if ("models" in nextMessage) {
969
+ nextMessage.models = ["{{modelId}}"];
970
+ }
971
+ if ("parentId" in nextMessage) {
972
+ nextMessage.parentId = "{{parentIdOrNull}}";
973
+ }
974
+ if ("parent_id" in nextMessage) {
975
+ nextMessage.parent_id = "{{parentIdOrNull}}";
976
+ }
977
+ if ("childrenIds" in nextMessage && Array.isArray(nextMessage.childrenIds)) {
978
+ nextMessage.childrenIds = [];
979
+ }
980
+ if ("children_ids" in nextMessage && Array.isArray(nextMessage.children_ids)) {
981
+ nextMessage.children_ids = [];
982
+ }
983
+ return nextMessage;
984
+ }
929
985
  function buildOpenAiConversationRequestBody(requestBody) {
930
986
  const base = requestBody ? structuredClone(requestBody) : {};
931
987
  const capturedMessage = Array.isArray(requestBody?.messages) &&
@@ -1516,6 +1572,7 @@ export const sessionPackageStoreModule = {
1516
1572
  sessionPackageMetadataSchema,
1517
1573
  installedProviderPackageSchema,
1518
1574
  createInMemorySessionPackageStore,
1575
+ buildQwenConversationRequestBody,
1519
1576
  buildSessionPackageStatus,
1520
1577
  cloneProvider,
1521
1578
  cloneConfig,
@@ -1,4 +1,13 @@
1
1
  export declare const cliModule: {
2
+ buildDetachedServerLaunchCommand: (input: {
3
+ scriptPath: string;
4
+ argv: string[];
5
+ execPath: string;
6
+ execArgv: string[];
7
+ }) => {
8
+ command: string;
9
+ args: string[];
10
+ };
2
11
  getBridgeServerCliHelpText: () => string;
3
12
  parseBridgeServerCliArgs: (input: {
4
13
  argv: string[];
@@ -1,5 +1,6 @@
1
1
  import { runBridgeServerCliModule } from "./run-bridge-server-cli.js";
2
2
  export const cliModule = {
3
+ buildDetachedServerLaunchCommand: runBridgeServerCliModule.buildDetachedServerLaunchCommand,
3
4
  getBridgeServerCliHelpText: runBridgeServerCliModule.getBridgeServerCliHelpText,
4
5
  parseBridgeServerCliArgs: runBridgeServerCliModule.parseBridgeServerCliArgs,
5
6
  runBridgeServerCli: runBridgeServerCliModule.runBridgeServerCli
@@ -8,6 +8,77 @@ type BridgeServerCliCommand = {
8
8
  } | {
9
9
  kind: "serve";
10
10
  config: BridgeServerConfig;
11
+ foreground: boolean;
12
+ } | {
13
+ kind: "status";
14
+ config: BridgeServerConfig;
15
+ } | {
16
+ kind: "stop";
17
+ config: BridgeServerConfig;
18
+ } | {
19
+ kind: "logs";
20
+ config: BridgeServerConfig;
21
+ follow: boolean;
22
+ lines: number;
23
+ } | {
24
+ kind: "providers-list";
25
+ baseUrl: string;
26
+ } | {
27
+ kind: "providers-get";
28
+ baseUrl: string;
29
+ id: string;
30
+ } | {
31
+ kind: "providers-add";
32
+ baseUrl: string;
33
+ id: string;
34
+ providerKind: string;
35
+ label: string;
36
+ enabled: boolean;
37
+ config: Record<string, unknown> | undefined;
38
+ } | {
39
+ kind: "providers-remove";
40
+ baseUrl: string;
41
+ id: string;
42
+ } | {
43
+ kind: "providers-enable";
44
+ baseUrl: string;
45
+ id: string;
46
+ } | {
47
+ kind: "providers-disable";
48
+ baseUrl: string;
49
+ id: string;
50
+ } | {
51
+ kind: "providers-import-session";
52
+ baseUrl: string;
53
+ id: string;
54
+ filePath?: string;
55
+ } | {
56
+ kind: "providers-session-status";
57
+ baseUrl: string;
58
+ id: string;
59
+ } | {
60
+ kind: "providers-clear-session";
61
+ baseUrl: string;
62
+ id: string;
63
+ } | {
64
+ kind: "models-list";
65
+ baseUrl: string;
66
+ } | {
67
+ kind: "models-add";
68
+ baseUrl: string;
69
+ provider: string;
70
+ model: string;
71
+ } | {
72
+ kind: "sessions-list";
73
+ baseUrl: string;
74
+ } | {
75
+ kind: "sessions-get";
76
+ baseUrl: string;
77
+ id: string;
78
+ } | {
79
+ kind: "sessions-remove";
80
+ baseUrl: string;
81
+ id: string;
11
82
  } | {
12
83
  kind: "clear-session-vault";
13
84
  config: BridgeServerConfig;
@@ -39,6 +110,20 @@ type RunBridgeServerCliInput = {
39
110
  keyPath: string;
40
111
  stdin: NodeJS.ReadStream;
41
112
  }) => Promise<string>;
113
+ spawnDetachedServerProcess?: (input: SpawnDetachedServerProcessInput) => Promise<SpawnDetachedServerProcessResult>;
114
+ };
115
+ type SpawnDetachedServerProcessInput = {
116
+ argv: string[];
117
+ env: NodeJS.ProcessEnv;
118
+ logPath: string;
119
+ cwd: string;
120
+ };
121
+ type SpawnDetachedServerProcessResult = {
122
+ pid: number;
123
+ };
124
+ type DetachedServerLaunchCommand = {
125
+ command: string;
126
+ args: string[];
42
127
  };
43
128
  declare function runBridgeServerCli(input: RunBridgeServerCliInput): Promise<number>;
44
129
  declare function parseBridgeServerCliArgs(input: {
@@ -46,7 +131,14 @@ declare function parseBridgeServerCliArgs(input: {
46
131
  env?: NodeJS.ProcessEnv;
47
132
  }): BridgeServerCliCommand;
48
133
  declare function getBridgeServerCliHelpText(): string;
134
+ declare function buildDetachedServerLaunchCommand(input: {
135
+ scriptPath: string;
136
+ argv: string[];
137
+ execPath: string;
138
+ execArgv: string[];
139
+ }): DetachedServerLaunchCommand;
49
140
  export declare const runBridgeServerCliModule: {
141
+ buildDetachedServerLaunchCommand: typeof buildDetachedServerLaunchCommand;
50
142
  runBridgeServerCli: typeof runBridgeServerCli;
51
143
  parseBridgeServerCliArgs: typeof parseBridgeServerCliArgs;
52
144
  getBridgeServerCliHelpText: typeof getBridgeServerCliHelpText;