llm-cli-gateway 2.4.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/README.md +18 -18
- package/dist/async-job-manager.d.ts +2 -0
- package/dist/async-job-manager.js +43 -3
- package/dist/auth.d.ts +44 -1
- package/dist/auth.js +60 -13
- package/dist/cli-updater.js +22 -13
- package/dist/config.d.ts +2 -0
- package/dist/config.js +151 -0
- package/dist/doctor.d.ts +15 -0
- package/dist/doctor.js +22 -11
- package/dist/executor.d.ts +1 -0
- package/dist/executor.js +7 -0
- package/dist/http-transport.js +74 -12
- package/dist/index.d.ts +16 -1
- package/dist/index.js +643 -306
- package/dist/oauth.d.ts +38 -0
- package/dist/oauth.js +441 -0
- package/dist/provider-codegen.d.ts +27 -0
- package/dist/provider-codegen.js +335 -0
- package/dist/provider-login-guidance.js +9 -9
- package/dist/provider-status.js +5 -5
- package/dist/request-context.d.ts +7 -0
- package/dist/request-context.js +8 -0
- package/dist/request-helpers.js +2 -2
- package/dist/upstream-contracts.js +95 -116
- package/dist/workspace-registry.d.ts +63 -0
- package/dist/workspace-registry.js +417 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/setup/status.schema.json +42 -1
package/dist/doctor.d.ts
CHANGED
|
@@ -69,6 +69,21 @@ export interface DoctorReport {
|
|
|
69
69
|
required: boolean;
|
|
70
70
|
token_configured: boolean;
|
|
71
71
|
source: string;
|
|
72
|
+
oauth: {
|
|
73
|
+
enabled: boolean;
|
|
74
|
+
registration_policy: string;
|
|
75
|
+
clients_configured: number;
|
|
76
|
+
shared_secret_enabled: boolean;
|
|
77
|
+
pkce_required: boolean;
|
|
78
|
+
issuer: string | null;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
workspaces: {
|
|
82
|
+
enabled: boolean;
|
|
83
|
+
default: string | null;
|
|
84
|
+
repo_count: number;
|
|
85
|
+
allowed_root_count: number;
|
|
86
|
+
gateway_app_dir_is_workspace: boolean;
|
|
72
87
|
};
|
|
73
88
|
providers: Record<"claude" | "codex" | "gemini" | "grok" | "mistral", {
|
|
74
89
|
cli_available: boolean;
|
package/dist/doctor.js
CHANGED
|
@@ -6,7 +6,8 @@ import { loadAuthConfig } from "./auth.js";
|
|
|
6
6
|
import { createEndpointExposureReport, redactDiagnosticUrl, } from "./endpoint-exposure.js";
|
|
7
7
|
import { listProviderRuntimeStatuses, } from "./provider-status.js";
|
|
8
8
|
import { CLAUDE_MCP_SERVER_NAMES } from "./claude-mcp-config.js";
|
|
9
|
-
import { loadCacheAwarenessConfig } from "./config.js";
|
|
9
|
+
import { loadCacheAwarenessConfig, loadRemoteOAuthConfig, } from "./config.js";
|
|
10
|
+
import { loadWorkspaceRegistry } from "./workspace-registry.js";
|
|
10
11
|
import { computeGlobalCacheStats } from "./cache-stats.js";
|
|
11
12
|
import { FlightRecorder, resolveFlightRecorderDbPath } from "./flight-recorder.js";
|
|
12
13
|
import { buildUpstreamContractReport } from "./upstream-contracts.js";
|
|
@@ -168,16 +169,7 @@ function chatGPTConnectorUrl(env, rawPublicUrl) {
|
|
|
168
169
|
.find(value => value.startsWith("/") && !value.includes("?") && !value.includes("#"));
|
|
169
170
|
if (!rawPublicUrl || !path)
|
|
170
171
|
return null;
|
|
171
|
-
|
|
172
|
-
const url = new URL(rawPublicUrl);
|
|
173
|
-
url.pathname = path;
|
|
174
|
-
url.search = "";
|
|
175
|
-
url.hash = "";
|
|
176
|
-
return redactDiagnosticUrl(url.toString());
|
|
177
|
-
}
|
|
178
|
-
catch {
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
172
|
+
return "<redacted>";
|
|
181
173
|
}
|
|
182
174
|
function buildCacheAwarenessReport(opts) {
|
|
183
175
|
const enabled = [];
|
|
@@ -240,6 +232,8 @@ export function createDoctorReport(envOrOptions = process.env) {
|
|
|
240
232
|
: { env: envOrOptions };
|
|
241
233
|
const env = opts.env ?? process.env;
|
|
242
234
|
const auth = loadAuthConfig(env);
|
|
235
|
+
const oauth = loadRemoteOAuthConfig(undefined, env);
|
|
236
|
+
const workspaceRegistry = loadWorkspaceRegistry();
|
|
243
237
|
const transport = defaultTransport(env);
|
|
244
238
|
const rawPublicUrl = env.LLM_GATEWAY_PUBLIC_URL || null;
|
|
245
239
|
const publicUrl = redactDiagnosticUrl(rawPublicUrl);
|
|
@@ -294,6 +288,23 @@ export function createDoctorReport(envOrOptions = process.env) {
|
|
|
294
288
|
required: auth.required,
|
|
295
289
|
token_configured: auth.tokenConfigured,
|
|
296
290
|
source: auth.source,
|
|
291
|
+
oauth: {
|
|
292
|
+
enabled: oauth.enabled,
|
|
293
|
+
registration_policy: oauth.registrationPolicy,
|
|
294
|
+
clients_configured: oauth.clients.length,
|
|
295
|
+
shared_secret_enabled: Boolean(oauth.sharedSecret?.enabled),
|
|
296
|
+
pkce_required: oauth.requirePkce,
|
|
297
|
+
issuer: oauth.issuer === "auto"
|
|
298
|
+
? publicUrl
|
|
299
|
+
: redactDiagnosticUrl(oauth.issuer === "auto" ? null : oauth.issuer),
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
workspaces: {
|
|
303
|
+
enabled: workspaceRegistry.enabled,
|
|
304
|
+
default: workspaceRegistry.defaultAlias,
|
|
305
|
+
repo_count: workspaceRegistry.repos.length,
|
|
306
|
+
allowed_root_count: workspaceRegistry.allowedRoots.length,
|
|
307
|
+
gateway_app_dir_is_workspace: workspaceRegistry.repos.some(repo => repo.path === join(homedir(), ".llm-cli-gateway")),
|
|
297
308
|
},
|
|
298
309
|
providers: {
|
|
299
310
|
claude: doctorProviderStatus(providerStatuses.claude),
|
package/dist/executor.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export interface ExecuteResult {
|
|
|
13
13
|
stderr: string;
|
|
14
14
|
code: number;
|
|
15
15
|
}
|
|
16
|
+
export declare function providerCommandName(command: string): string;
|
|
16
17
|
export declare function buildExtendedPath(env?: NodeJS.ProcessEnv, home?: string, nodePath?: string, platform?: NodeJS.Platform): string;
|
|
17
18
|
export declare function getExtendedPath(): string;
|
|
18
19
|
export declare function envWithExtendedPath(baseEnv?: NodeJS.ProcessEnv, extendedPath?: string, platform?: NodeJS.Platform): NodeJS.ProcessEnv;
|
package/dist/executor.js
CHANGED
|
@@ -3,6 +3,13 @@ import { homedir } from "os";
|
|
|
3
3
|
import { delimiter, join, dirname, extname, win32 } from "path";
|
|
4
4
|
import { readdirSync, existsSync } from "fs";
|
|
5
5
|
import { createCircuitBreaker, withRetry } from "./retry.js";
|
|
6
|
+
export function providerCommandName(command) {
|
|
7
|
+
if (command === "gemini")
|
|
8
|
+
return "agy";
|
|
9
|
+
if (command === "mistral")
|
|
10
|
+
return "vibe";
|
|
11
|
+
return command;
|
|
12
|
+
}
|
|
6
13
|
const MAX_OUTPUT_SIZE = 50 * 1024 * 1024;
|
|
7
14
|
const circuitBreakers = new Map();
|
|
8
15
|
let cachedNvmPath;
|
package/dist/http-transport.js
CHANGED
|
@@ -3,31 +3,42 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
4
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import { authorizeBearerRequest, getRequiredBearerToken, writeAuthFailure } from "./auth.js";
|
|
6
|
+
import { loadRemoteOAuthConfig } from "./config.js";
|
|
7
|
+
import { OAuthServer, oauthBaseUrlFromRequest } from "./oauth.js";
|
|
8
|
+
import { runWithRequestContext } from "./request-context.js";
|
|
6
9
|
const noopLogger = {
|
|
7
10
|
info: (..._args) => { },
|
|
8
11
|
error: (..._args) => { },
|
|
9
12
|
debug: (..._args) => { },
|
|
10
13
|
};
|
|
11
|
-
function
|
|
14
|
+
function firstHeader(value) {
|
|
15
|
+
return Array.isArray(value) ? value[0] : value;
|
|
16
|
+
}
|
|
17
|
+
function readRawBody(req) {
|
|
12
18
|
return new Promise((resolve, reject) => {
|
|
13
19
|
const chunks = [];
|
|
14
20
|
req.on("data", chunk => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
15
21
|
req.on("error", reject);
|
|
16
22
|
req.on("end", () => {
|
|
17
23
|
if (chunks.length === 0) {
|
|
18
|
-
resolve(
|
|
24
|
+
resolve("");
|
|
19
25
|
return;
|
|
20
26
|
}
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
resolve(JSON.parse(raw));
|
|
24
|
-
}
|
|
25
|
-
catch (error) {
|
|
26
|
-
reject(error);
|
|
27
|
-
}
|
|
27
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
28
28
|
});
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
|
+
async function readBody(req) {
|
|
32
|
+
const raw = await readRawBody(req);
|
|
33
|
+
if (!raw)
|
|
34
|
+
return undefined;
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(raw);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
31
42
|
function methodNotAllowed(res) {
|
|
32
43
|
res.writeHead(405, { allow: "GET, POST, DELETE", "content-type": "application/json" });
|
|
33
44
|
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
@@ -36,6 +47,10 @@ function jsonError(res, status, message) {
|
|
|
36
47
|
res.writeHead(status, { "content-type": "application/json" });
|
|
37
48
|
res.end(JSON.stringify({ error: message }));
|
|
38
49
|
}
|
|
50
|
+
function jsonResponse(res, status, body) {
|
|
51
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
52
|
+
res.end(JSON.stringify(body));
|
|
53
|
+
}
|
|
39
54
|
function parseNoAuthPaths(raw, protectedPath) {
|
|
40
55
|
const paths = new Set();
|
|
41
56
|
for (const value of (raw ?? "").split(/[,;\s]+/)) {
|
|
@@ -51,6 +66,25 @@ function parseNoAuthPaths(raw, protectedPath) {
|
|
|
51
66
|
}
|
|
52
67
|
return paths;
|
|
53
68
|
}
|
|
69
|
+
function isLocalHost(host) {
|
|
70
|
+
const hostname = host.split(":")[0]?.toLowerCase() ?? "";
|
|
71
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
72
|
+
}
|
|
73
|
+
function requestBaseUrl(req) {
|
|
74
|
+
const configured = process.env.LLM_GATEWAY_PUBLIC_URL;
|
|
75
|
+
if (configured) {
|
|
76
|
+
try {
|
|
77
|
+
return new URL(configured).origin;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const host = firstHeader(req.headers.host) ?? "127.0.0.1:3333";
|
|
83
|
+
const forwardedProto = firstHeader(req.headers["x-forwarded-proto"]);
|
|
84
|
+
const proto = forwardedProto ??
|
|
85
|
+
(host.startsWith("127.0.0.1") || host.startsWith("localhost") ? "http" : "https");
|
|
86
|
+
return `${proto}://${host}`;
|
|
87
|
+
}
|
|
54
88
|
export async function startHttpGateway(options) {
|
|
55
89
|
const host = options.host ?? process.env.LLM_GATEWAY_HTTP_HOST ?? "127.0.0.1";
|
|
56
90
|
const port = options.port ?? Number(process.env.LLM_GATEWAY_HTTP_PORT ?? 3333);
|
|
@@ -59,6 +93,10 @@ export async function startHttpGateway(options) {
|
|
|
59
93
|
const logger = options.logger ?? noopLogger;
|
|
60
94
|
const sessions = new Map();
|
|
61
95
|
const token = getRequiredBearerToken();
|
|
96
|
+
const oauthConfig = loadRemoteOAuthConfig(logger);
|
|
97
|
+
const oauthServer = oauthConfig.enabled
|
|
98
|
+
? new OAuthServer({ protectedPath: path, config: oauthConfig, logger })
|
|
99
|
+
: null;
|
|
62
100
|
async function closeSession(sessionId) {
|
|
63
101
|
const entry = sessions.get(sessionId);
|
|
64
102
|
if (!entry)
|
|
@@ -89,22 +127,46 @@ export async function startHttpGateway(options) {
|
|
|
89
127
|
const httpServer = createServer(async (req, res) => {
|
|
90
128
|
try {
|
|
91
129
|
const url = new URL(req.url || "/", `http://${req.headers.host || `${host}:${port}`}`);
|
|
130
|
+
const baseUrl = requestBaseUrl(req);
|
|
131
|
+
const oauthOrigin = oauthServer ? oauthBaseUrlFromRequest(req, oauthConfig) : null;
|
|
132
|
+
const effectiveOAuthBaseUrl = oauthOrigin ?? baseUrl;
|
|
133
|
+
const resourceMetadataUrl = oauthServer && oauthOrigin ? oauthServer.resourceMetadataUrl(oauthOrigin) : undefined;
|
|
92
134
|
if (url.pathname === "/healthz") {
|
|
93
135
|
res.writeHead(200, { "content-type": "application/json" });
|
|
94
136
|
res.end(JSON.stringify({ ok: true, sessions: sessions.size }));
|
|
95
137
|
return;
|
|
96
138
|
}
|
|
139
|
+
if (oauthServer) {
|
|
140
|
+
if (oauthServer.isOAuthPath(url.pathname) && !oauthOrigin) {
|
|
141
|
+
jsonError(res, 503, "LLM_GATEWAY_PUBLIC_URL is required for public OAuth issuer metadata");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (await oauthServer.handle({
|
|
145
|
+
req,
|
|
146
|
+
res,
|
|
147
|
+
url,
|
|
148
|
+
baseUrl: effectiveOAuthBaseUrl,
|
|
149
|
+
})) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
97
153
|
const noAuthPath = noAuthPaths.has(url.pathname);
|
|
98
154
|
if (url.pathname !== path && !noAuthPath) {
|
|
99
155
|
jsonError(res, 404, "Not found");
|
|
100
156
|
return;
|
|
101
157
|
}
|
|
158
|
+
let requestContext = { authScopes: [] };
|
|
102
159
|
if (!noAuthPath) {
|
|
103
160
|
const auth = authorizeBearerRequest(req, token);
|
|
104
161
|
if (!auth.ok) {
|
|
105
|
-
writeAuthFailure(res, auth);
|
|
162
|
+
writeAuthFailure(res, auth, resourceMetadataUrl ? { resourceMetadataUrl } : {});
|
|
106
163
|
return;
|
|
107
164
|
}
|
|
165
|
+
requestContext = {
|
|
166
|
+
authKind: auth.kind,
|
|
167
|
+
authScopes: auth.scopes ?? [],
|
|
168
|
+
authClientId: auth.clientId,
|
|
169
|
+
};
|
|
108
170
|
}
|
|
109
171
|
if (req.method !== "GET" && req.method !== "POST" && req.method !== "DELETE") {
|
|
110
172
|
methodNotAllowed(res);
|
|
@@ -129,7 +191,7 @@ export async function startHttpGateway(options) {
|
|
|
129
191
|
return;
|
|
130
192
|
}
|
|
131
193
|
const body = req.method === "POST" ? await readBody(req) : undefined;
|
|
132
|
-
await entry.transport.handleRequest(req, res, body);
|
|
194
|
+
await runWithRequestContext(requestContext, () => entry.transport.handleRequest(req, res, body));
|
|
133
195
|
return;
|
|
134
196
|
}
|
|
135
197
|
if (req.method !== "POST") {
|
|
@@ -146,7 +208,7 @@ export async function startHttpGateway(options) {
|
|
|
146
208
|
return;
|
|
147
209
|
}
|
|
148
210
|
const entry = await createSession();
|
|
149
|
-
await entry.transport.handleRequest(req, res, body);
|
|
211
|
+
await runWithRequestContext(requestContext, () => entry.transport.handleRequest(req, res, body));
|
|
150
212
|
}
|
|
151
213
|
catch (error) {
|
|
152
214
|
logger.error("HTTP transport request failed", error);
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { ClaudeMcpConfigResult, ClaudeMcpServerName } from "./claude-mcp-config.
|
|
|
14
14
|
import { type MistralAgentMode, type ClaudePermissionMode, type CodexSandboxMode, type CodexAskForApproval, type ClaudeEffortLevel } from "./request-helpers.js";
|
|
15
15
|
import { FlightRecorderLike } from "./flight-recorder.js";
|
|
16
16
|
import { type PromptParts } from "./prompt-parts.js";
|
|
17
|
+
import { type WorkspaceRegistry } from "./workspace-registry.js";
|
|
17
18
|
export interface WarningEntry {
|
|
18
19
|
code: string;
|
|
19
20
|
message?: string;
|
|
@@ -59,6 +60,7 @@ export declare const WORKTREE_SCHEMA: z.ZodUnion<[z.ZodBoolean, z.ZodObject<{
|
|
|
59
60
|
name?: string | undefined;
|
|
60
61
|
ref?: string | undefined;
|
|
61
62
|
}>]>;
|
|
63
|
+
export declare const WORKSPACE_ALIAS_SCHEMA: z.ZodString;
|
|
62
64
|
export declare const SESSION_PROVIDER_VALUES: readonly ["claude", "codex", "gemini", "grok", "mistral", "grok-api"];
|
|
63
65
|
export declare const SESSION_PROVIDER_ENUM: z.ZodEnum<["claude", "codex", "gemini", "grok", "mistral", "grok-api"]>;
|
|
64
66
|
export type SessionProvider = ProviderType;
|
|
@@ -74,6 +76,7 @@ export interface GatewayServerDeps {
|
|
|
74
76
|
persistence?: PersistenceConfig;
|
|
75
77
|
cacheAwareness?: CacheAwarenessConfig;
|
|
76
78
|
providers?: ProvidersConfig;
|
|
79
|
+
workspaces?: WorkspaceRegistry;
|
|
77
80
|
}
|
|
78
81
|
export interface GatewayServerRuntime {
|
|
79
82
|
sessionManager: ISessionManager;
|
|
@@ -87,6 +90,7 @@ export interface GatewayServerRuntime {
|
|
|
87
90
|
persistence: PersistenceConfig;
|
|
88
91
|
cacheAwareness: CacheAwarenessConfig;
|
|
89
92
|
providers: ProvidersConfig;
|
|
93
|
+
workspaces: WorkspaceRegistry;
|
|
90
94
|
}
|
|
91
95
|
export declare function resolveGatewayServerRuntime(deps?: GatewayServerDeps, options?: {
|
|
92
96
|
isolateState?: boolean;
|
|
@@ -95,11 +99,17 @@ export declare function shouldRegisterGrokApiTools(providers: ProvidersConfig):
|
|
|
95
99
|
export interface ResolvedWorktree {
|
|
96
100
|
cwd?: string;
|
|
97
101
|
worktreePath?: string;
|
|
102
|
+
workspaceAlias?: string;
|
|
103
|
+
workspaceRoot?: string;
|
|
98
104
|
}
|
|
99
105
|
export declare function resolveWorktreeForRequest(worktreeOpt: boolean | {
|
|
100
106
|
name?: string;
|
|
101
107
|
ref?: string;
|
|
102
|
-
} | undefined, sessionId: string | undefined, runtime: GatewayServerRuntime
|
|
108
|
+
} | undefined, sessionId: string | undefined, runtime: GatewayServerRuntime, options?: {
|
|
109
|
+
repoRoot?: string;
|
|
110
|
+
workspaceAlias?: string;
|
|
111
|
+
workspaceRoot?: string;
|
|
112
|
+
}): Promise<ResolvedWorktree>;
|
|
103
113
|
export declare function formatWorktreePrefix(worktreePath?: string): string;
|
|
104
114
|
export declare function extractUsageAndCost(cli: "claude" | "codex" | "gemini" | "grok" | "mistral", output: string, outputFormat?: string, ctx?: {
|
|
105
115
|
sessionId?: string;
|
|
@@ -330,6 +340,7 @@ export interface GeminiRequestParams {
|
|
|
330
340
|
attachments?: string[];
|
|
331
341
|
skipTrust?: boolean;
|
|
332
342
|
yolo?: boolean;
|
|
343
|
+
workspace?: string;
|
|
333
344
|
worktree?: boolean | {
|
|
334
345
|
name?: string;
|
|
335
346
|
ref?: string;
|
|
@@ -343,6 +354,7 @@ export interface HandlerDeps {
|
|
|
343
354
|
error: (...args: any[]) => void;
|
|
344
355
|
debug: (...args: any[]) => void;
|
|
345
356
|
};
|
|
357
|
+
workspaces?: WorkspaceRegistry;
|
|
346
358
|
runtime?: GatewayServerRuntime;
|
|
347
359
|
}
|
|
348
360
|
export interface AsyncHandlerDeps extends HandlerDeps {
|
|
@@ -400,6 +412,7 @@ export interface GrokRequestParams {
|
|
|
400
412
|
restoreCode?: boolean;
|
|
401
413
|
leaderSocket?: string;
|
|
402
414
|
nativeWorktree?: boolean | string;
|
|
415
|
+
workspace?: string;
|
|
403
416
|
worktree?: boolean | {
|
|
404
417
|
name?: string;
|
|
405
418
|
ref?: string;
|
|
@@ -432,6 +445,7 @@ export interface MistralRequestParams {
|
|
|
432
445
|
maxTokens?: number;
|
|
433
446
|
workingDir?: string;
|
|
434
447
|
addDir?: string[];
|
|
448
|
+
workspace?: string;
|
|
435
449
|
worktree?: boolean | {
|
|
436
450
|
name?: string;
|
|
437
451
|
ref?: string;
|
|
@@ -469,6 +483,7 @@ export declare function handleCodexRequestAsync(deps: AsyncHandlerDeps, params:
|
|
|
469
483
|
ignoreRules?: boolean;
|
|
470
484
|
workingDir?: string;
|
|
471
485
|
addDir?: string[];
|
|
486
|
+
workspace?: string;
|
|
472
487
|
worktree?: boolean | {
|
|
473
488
|
name?: string;
|
|
474
489
|
ref?: string;
|