e2e 0.16.0 → 0.18.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/README.md +10 -0
- package/dist/.build.json +1 -1
- package/dist/agent/act.js +7 -4
- package/dist/agent/action-dispatcher.js +4 -5
- package/dist/agent/actions.d.ts +32 -23
- package/dist/agent/actions.js +38 -24
- package/dist/agent/ai-sdk.js +5 -4
- package/dist/agent/error.js +3 -3
- package/dist/agent/invocation.js +3 -4
- package/dist/agent/model/sdk.js +2 -0
- package/dist/agent/model/tool-choice.d.ts +9 -0
- package/dist/agent/model/tool-choice.js +14 -0
- package/dist/agent/observation-feed.d.ts +5 -6
- package/dist/agent/observation-feed.js +8 -11
- package/dist/agent/observation.d.ts +40 -11
- package/dist/agent/observation.js +109 -59
- package/dist/agent/phases.d.ts +7 -1
- package/dist/agent/phases.js +12 -3
- package/dist/agent/point-tap.d.ts +8 -8
- package/dist/agent/point-tap.js +1 -1
- package/dist/agent/prompts.js +3 -0
- package/dist/agent/replay.d.ts +19 -8
- package/dist/agent/replay.js +29 -29
- package/dist/agent/scroll-target.d.ts +2 -3
- package/dist/agent/step-cache.d.ts +43 -15
- package/dist/agent/step-cache.js +156 -67
- package/dist/agent/tool-loop.js +94 -5
- package/dist/cache/anchors.d.ts +72 -23
- package/dist/cache/anchors.js +233 -60
- package/dist/cache/context.d.ts +25 -14
- package/dist/cache/context.js +50 -8
- package/dist/cache/decide.d.ts +2 -2
- package/dist/cache/decide.js +4 -6
- package/dist/cache/identity.d.ts +18 -4
- package/dist/cache/identity.js +9 -4
- package/dist/cache/recorder.d.ts +15 -1
- package/dist/cache/recorder.js +36 -23
- package/dist/cache/rekeyed.d.ts +46 -0
- package/dist/cache/rekeyed.js +89 -0
- package/dist/cache/relocate.d.ts +14 -14
- package/dist/cache/relocate.js +33 -25
- package/dist/cache/route.d.ts +32 -33
- package/dist/cache/route.js +94 -50
- package/dist/cache/store.d.ts +6 -0
- package/dist/cache/store.js +21 -1
- package/dist/cache/trace.d.ts +53 -14
- package/dist/cache/trace.js +76 -30
- package/dist/cli/bin.js +2 -2
- package/dist/cli/index.js +43 -32
- package/dist/cli/init/engines.d.ts +1 -3
- package/dist/cli/init/engines.js +2 -2
- package/dist/cli/init/gateways.d.ts +1 -1
- package/dist/cli/init/gateways.js +11 -1
- package/dist/cli/init/mcp-config.d.ts +2 -1
- package/dist/cli/init/mcp-config.js +8 -3
- package/dist/cli/init/sibling-versions.json +1 -1
- package/dist/cli/init/versions.d.ts +0 -8
- package/dist/cli/init/versions.js +1 -10
- package/dist/cli/mcp.d.ts +3 -2
- package/dist/cli/mcp.js +52 -16
- package/dist/cli/run-output.d.ts +17 -0
- package/dist/cli/run-output.js +45 -0
- package/dist/collect/collect.js +8 -4
- package/dist/collect/registry.js +27 -16
- package/dist/config/compiled-files.d.ts +36 -0
- package/dist/config/compiled-files.js +50 -0
- package/dist/config/diagnose.js +21 -19
- package/dist/config/esm-hooks.d.ts +39 -44
- package/dist/config/esm-hooks.js +238 -63
- package/dist/config/load.d.ts +1 -7
- package/dist/config/load.js +23 -66
- package/dist/config/register.d.ts +8 -0
- package/dist/config/register.js +9 -0
- package/dist/config/resolve.d.ts +1 -0
- package/dist/config/resolve.js +33 -1
- package/dist/config/specifiers.d.ts +19 -0
- package/dist/config/specifiers.js +33 -0
- package/dist/config/tsconfig.d.ts +56 -0
- package/dist/config/tsconfig.js +115 -0
- package/dist/config/typescript.d.ts +19 -0
- package/dist/config/typescript.js +253 -0
- package/dist/engine/contract.d.ts +1 -0
- package/dist/engine/index.d.ts +4 -2
- package/dist/engine/session.js +2 -2
- package/dist/expect/async.js +4 -2
- package/dist/expect/poll-scope.d.ts +52 -0
- package/dist/expect/poll-scope.js +113 -0
- package/dist/expect/poll.js +33 -6
- package/dist/expect/values.js +11 -4
- package/dist/internal/abandoned.d.ts +11 -0
- package/dist/internal/abandoned.js +23 -0
- package/dist/internal/brands.d.ts +2 -0
- package/dist/internal/brands.js +2 -0
- package/dist/internal/client-identity.d.ts +9 -0
- package/dist/internal/client-identity.js +14 -0
- package/dist/internal/errors.d.ts +10 -3
- package/dist/internal/errors.js +23 -8
- package/dist/internal/node-version.d.ts +16 -7
- package/dist/internal/node-version.js +50 -15
- package/dist/internal/package-manager.js +1 -1
- package/dist/internal/realm-slot.d.ts +5 -4
- package/dist/internal/redact.d.ts +9 -4
- package/dist/internal/redact.js +120 -23
- package/dist/internal/time.d.ts +26 -4
- package/dist/internal/time.js +58 -14
- package/dist/internal/urls.d.ts +9 -5
- package/dist/internal/urls.js +19 -10
- package/dist/locator/engine.d.ts +12 -2
- package/dist/locator/engine.js +55 -14
- package/dist/locator/screen.js +59 -9
- package/dist/mcp/catalog.d.ts +3 -2
- package/dist/mcp/catalog.js +6 -6
- package/dist/mcp/output.d.ts +59 -0
- package/dist/mcp/output.js +129 -0
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.js +9 -12
- package/dist/mcp/session.d.ts +8 -1
- package/dist/mcp/session.js +56 -15
- package/dist/mcp/tools.js +7 -5
- package/dist/oauth/chatgpt.js +3 -23
- package/dist/oauth/cli.js +1 -0
- package/dist/oauth/copilot.d.ts +5 -4
- package/dist/oauth/copilot.js +80 -7
- package/dist/oauth/grok.js +2 -1
- package/dist/oauth/models.js +2 -1
- package/dist/oauth/opencode-console.d.ts +10 -0
- package/dist/oauth/opencode-console.js +133 -0
- package/dist/oauth/providers/github-copilot.d.ts +12 -0
- package/dist/oauth/providers/github-copilot.js +77 -23
- package/dist/oauth/providers/opencode-console.d.ts +31 -0
- package/dist/oauth/providers/opencode-console.js +148 -0
- package/dist/oauth/providers.d.ts +2 -6
- package/dist/oauth/providers.js +3 -7
- package/dist/oauth/responses.d.ts +9 -0
- package/dist/oauth/responses.js +23 -0
- package/dist/report/build.d.ts +26 -0
- package/dist/report/build.js +16 -8
- package/dist/report/failure-text.d.ts +6 -1
- package/dist/report/failure-text.js +14 -4
- package/dist/report/format.d.ts +10 -5
- package/dist/report/format.js +12 -5
- package/dist/report/junit.js +11 -12
- package/dist/report/list.d.ts +12 -0
- package/dist/report/list.js +61 -24
- package/dist/report/markdown.d.ts +1 -1
- package/dist/report/markdown.js +37 -14
- package/dist/report/repeats.d.ts +6 -1
- package/dist/report/repeats.js +12 -3
- package/dist/run/artifacts.d.ts +22 -4
- package/dist/run/artifacts.js +71 -3
- package/dist/run/execute.d.ts +34 -5
- package/dist/run/execute.js +93 -44
- package/dist/run/extended-fixtures.js +6 -2
- package/dist/run/failure-evidence.js +10 -4
- package/dist/run/fixtures.js +6 -8
- package/dist/run/in-process.d.ts +2 -0
- package/dist/run/in-process.js +52 -10
- package/dist/run/last-run.d.ts +42 -8
- package/dist/run/last-run.js +121 -14
- package/dist/run/process-output.d.ts +73 -0
- package/dist/run/process-output.js +130 -0
- package/dist/run/provision.d.ts +2 -0
- package/dist/run/provision.js +1 -0
- package/dist/run/realm.d.ts +4 -1
- package/dist/run/realm.js +20 -6
- package/dist/run/records.d.ts +20 -4
- package/dist/run/records.js +26 -0
- package/dist/run/retry.d.ts +10 -1
- package/dist/run/retry.js +30 -12
- package/dist/run/runner.d.ts +9 -1
- package/dist/run/runner.js +101 -39
- package/dist/run/scheduler.js +255 -19
- package/dist/run/secrecy.d.ts +22 -19
- package/dist/run/secrecy.js +22 -18
- package/dist/run/serial.d.ts +75 -2
- package/dist/run/serial.js +243 -59
- package/dist/run/standalone.js +1 -1
- package/dist/run/steps.d.ts +0 -6
- package/dist/run/steps.js +3 -20
- package/dist/run/trace-redaction.d.ts +5 -6
- package/dist/run/trace-redaction.js +5 -6
- package/dist/run/worker/entry.js +40 -18
- package/dist/run/worker/handle.js +10 -5
- package/dist/run/worker/protocol.d.ts +85 -3
- package/dist/run/worker/session.d.ts +2 -0
- package/dist/run/worker/session.js +29 -0
- package/dist/telemetry/environment.d.ts +2 -1
- package/dist/telemetry/environment.js +12 -2
- package/dist/telemetry/events.js +1 -0
- package/dist/telemetry/project.d.ts +5 -6
- package/dist/telemetry/project.js +6 -11
- package/dist/telemetry/store.d.ts +5 -5
- package/dist/telemetry/store.js +5 -7
- package/dist/telemetry/telemetry.d.ts +2 -3
- package/dist/telemetry/telemetry.js +2 -4
- package/dist/types.d.ts +15 -6
- package/docs/agent-steps.mdx +1 -0
- package/docs/agents.mdx +7 -3
- package/docs/api-testing.mdx +2 -1
- package/docs/authentication.mdx +6 -3
- package/docs/browser.mdx +49 -12
- package/docs/bug-bash.mdx +2 -1
- package/docs/cache.mdx +66 -19
- package/docs/ci.mdx +14 -9
- package/docs/coding-agents.mdx +2 -1
- package/docs/core-concepts.mdx +208 -0
- package/docs/debugging.mdx +25 -12
- package/docs/decision-models.mdx +142 -0
- package/docs/example-projects.mdx +32 -0
- package/docs/executors.mdx +7 -2
- package/docs/explore.mdx +1 -0
- package/docs/github.mdx +14 -5
- package/docs/index.mdx +9 -5
- package/docs/integrations/eas.mdx +1 -0
- package/docs/integrations/index.mdx +1 -0
- package/docs/integrations/kernel.mdx +3 -2
- package/docs/migrate/cypress.mdx +26 -3
- package/docs/migrate/detox.mdx +24 -1
- package/docs/migrate/maestro.mdx +28 -4
- package/docs/migrate/playwright.mdx +44 -18
- package/docs/migrate/selenium.mdx +27 -3
- package/docs/mobile-ci.mdx +6 -5
- package/docs/mobile.mdx +96 -8
- package/docs/models.mdx +60 -9
- package/docs/quickstart.mdx +28 -16
- package/docs/reference/agent.mdx +13 -3
- package/docs/reference/app.mdx +2 -1
- package/docs/reference/cli.mdx +41 -18
- package/docs/reference/config.mdx +67 -13
- package/docs/reference/engine.mdx +25 -13
- package/docs/reference/environment.mdx +10 -2
- package/docs/reference/errors.mdx +13 -10
- package/docs/reference/expect.mdx +36 -13
- package/docs/reference/mcp.mdx +21 -8
- package/docs/reference/mobile.mdx +24 -13
- package/docs/reference/reporters.mdx +9 -2
- package/docs/reference/screen.mdx +49 -16
- package/docs/reference/test.mdx +32 -5
- package/docs/reference/web.mdx +133 -23
- package/docs/security.mdx +58 -42
- package/docs/subscriptions.mdx +92 -12
- package/docs/telemetry.mdx +9 -6
- package/docs/tools.mdx +1 -0
- package/docs/{starting-your-app.mdx → web.mdx} +157 -11
- package/docs/writing-an-engine.mdx +2 -1
- package/package.json +25 -8
- package/schema/report-v1.schema.json +14 -1
- package/skills/e2e/SKILL.md +5 -3
- package/skills/e2e/references/agent.md +33 -11
- package/skills/e2e/references/debugging.md +4 -4
- package/skills/e2e/references/mcp.md +8 -3
- package/skills/e2e/references/running.md +15 -9
- package/skills/e2e/references/setup.md +45 -16
- package/skills/e2e/references/writing-tests.md +35 -18
- package/docs/viewports.mdx +0 -146
- package/docs/writing-tests.mdx +0 -256
package/dist/mcp/session.js
CHANGED
|
@@ -21,7 +21,7 @@ import { LocatorEngine } from '../locator/engine.js';
|
|
|
21
21
|
import { allocateAppPorts } from '../run/app-ports.js';
|
|
22
22
|
import { SharedAppProcesses } from '../run/process-pool.js';
|
|
23
23
|
import { outputLayout } from '../run/output.js';
|
|
24
|
-
import { sessionSecrecy } from '../run/secrecy.js';
|
|
24
|
+
import { processSecrets, registerStaticSecrets, sessionSecrecy } from '../run/secrecy.js';
|
|
25
25
|
import { openStandaloneAttempt } from '../run/standalone.js';
|
|
26
26
|
import { createSessionCatalog, isGrammarVerb } from './catalog.js';
|
|
27
27
|
import { describeRecording, SessionRecorder } from './recording.js';
|
|
@@ -55,9 +55,13 @@ export class SessionHost {
|
|
|
55
55
|
get maxSessions() {
|
|
56
56
|
return this.options.maxSessions ?? SESSION_BOUNDS.default;
|
|
57
57
|
}
|
|
58
|
-
/** The server's tools: the same four whatever the project, the config, or the target. */
|
|
58
|
+
/** The server's tools: the same four whatever the project, the config, or the target, each redacting what it returns. */
|
|
59
59
|
toolSpecs() {
|
|
60
|
-
return [this.openSpec(), this.catalogSpec(), this.callSpec(), this.closeSpec()];
|
|
60
|
+
return [this.openSpec(), this.catalogSpec(), this.callSpec(), this.closeSpec()].map(redacting);
|
|
61
|
+
}
|
|
62
|
+
/** Logs a line for the operator and the client, redacted with every secret value the process knows. */
|
|
63
|
+
log(level, message) {
|
|
64
|
+
this.options.log(level, processSecrets.redact(message));
|
|
61
65
|
}
|
|
62
66
|
/**
|
|
63
67
|
* Opens a session and returns its opening text: the summary, the catalog,
|
|
@@ -65,8 +69,9 @@ export class SessionHost {
|
|
|
65
69
|
* it, or disconnects, aborts the open and what it started.
|
|
66
70
|
*/
|
|
67
71
|
open(options, signal) {
|
|
68
|
-
const
|
|
69
|
-
|
|
72
|
+
const headed = options.headed ?? this.options.headed;
|
|
73
|
+
const usage = new SessionUsage(this.sessions.liveCount, headed);
|
|
74
|
+
return this.sessions.admit((id) => this.openSession(id, options, headed, usage, signal)).catch((cause) => {
|
|
70
75
|
this.options.onSessionEnd?.(usage.openFailed(classifyError(cause).code));
|
|
71
76
|
throw cause;
|
|
72
77
|
});
|
|
@@ -99,7 +104,8 @@ export class SessionHost {
|
|
|
99
104
|
lines.push(`The session step ended with: ${errorMessage(value.outcome.error)}`);
|
|
100
105
|
for (const error of cleanupErrors)
|
|
101
106
|
lines.push(`Cleanup: ${error.code}: ${error.message}`);
|
|
102
|
-
|
|
107
|
+
// Redacted here, not only at the tool boundary: closeAll writes it to stderr.
|
|
108
|
+
return processSecrets.redact(lines.join('\n'));
|
|
103
109
|
}
|
|
104
110
|
/**
|
|
105
111
|
* Runs `settle` (ending the step, saving a recording), then closes the
|
|
@@ -149,7 +155,7 @@ export class SessionHost {
|
|
|
149
155
|
return redactResult(errorResult(cause), redact);
|
|
150
156
|
});
|
|
151
157
|
}
|
|
152
|
-
async openSession(id, options, usage, request) {
|
|
158
|
+
async openSession(id, options, headed, usage, request) {
|
|
153
159
|
// The catalog renders synchronously, so the optional SDK is loaded once
|
|
154
160
|
// here when installed. Without it the catalog reads the tools' Standard
|
|
155
161
|
// Schemas, and only a model-backed call needs the package.
|
|
@@ -159,7 +165,16 @@ export class SessionHost {
|
|
|
159
165
|
// knows that session's config.
|
|
160
166
|
const configPath = this.options.locateConfig(options.config);
|
|
161
167
|
this.sessions.claimConfig(id, configPath);
|
|
162
|
-
const
|
|
168
|
+
const withhold = this.options.withholdOutput ?? ((load) => load());
|
|
169
|
+
const loaded = await withhold(async () => {
|
|
170
|
+
const fresh = await this.options.loadConfig(configPath);
|
|
171
|
+
// Known to the process before anything can fail with one, so an open
|
|
172
|
+
// failure and what the config printed while it loaded are redacted
|
|
173
|
+
// like any other text; a session registers them only once its engine
|
|
174
|
+
// has launched.
|
|
175
|
+
registerStaticSecrets(fresh.allSecrets);
|
|
176
|
+
return fresh;
|
|
177
|
+
});
|
|
163
178
|
// A session is its own run: a URL declared with port 0 gets a port here.
|
|
164
179
|
const config = await allocateAppPorts(loaded);
|
|
165
180
|
const target = this.resolveTarget(config, options.target);
|
|
@@ -184,12 +199,12 @@ export class SessionHost {
|
|
|
184
199
|
// one attempt that closes as passed, so it traces under `on` only.
|
|
185
200
|
config,
|
|
186
201
|
target: { ...target, video: { mode: 'off', source: 'default' } },
|
|
187
|
-
headed
|
|
202
|
+
headed,
|
|
188
203
|
env: this.options.env,
|
|
189
204
|
signal: abort.signal,
|
|
190
205
|
timeoutMs: ttlMs + CLOSE_GRACE_MS,
|
|
191
206
|
processes: this.apps,
|
|
192
|
-
notice: (scope, message) => this.
|
|
207
|
+
notice: (scope, message) => this.log('info', `${scope}: ${message}`),
|
|
193
208
|
});
|
|
194
209
|
// The coding agent is the brain: the step is driven from here, and the
|
|
195
210
|
// configured model stays out of the way.
|
|
@@ -207,7 +222,7 @@ export class SessionHost {
|
|
|
207
222
|
screen,
|
|
208
223
|
session: attempt.session,
|
|
209
224
|
tools: config.agent.tools,
|
|
210
|
-
redact: attempt.agentRuntime.redact,
|
|
225
|
+
redaction: { redact: attempt.agentRuntime.redact, redactCut: attempt.agentRuntime.redactCut },
|
|
211
226
|
recorder,
|
|
212
227
|
locator: new LocatorEngine({
|
|
213
228
|
session: attempt.session,
|
|
@@ -217,13 +232,14 @@ export class SessionHost {
|
|
|
217
232
|
actionTimeout: config.actionTimeout,
|
|
218
233
|
assertionTimeout: config.assertionTimeout,
|
|
219
234
|
}),
|
|
220
|
-
warn: (message) => this.
|
|
235
|
+
warn: (message) => this.log('warning', message),
|
|
221
236
|
onActionFailed: (cause) => usage.failed(classifyError(cause).code),
|
|
222
237
|
});
|
|
223
238
|
const live = {
|
|
224
239
|
id,
|
|
225
240
|
target,
|
|
226
241
|
configPath: loaded.configPath,
|
|
242
|
+
headed,
|
|
227
243
|
attempt,
|
|
228
244
|
step,
|
|
229
245
|
screen,
|
|
@@ -257,8 +273,8 @@ export class SessionHost {
|
|
|
257
273
|
endOnItsOwn(live, why, endedBy) {
|
|
258
274
|
if (!this.sessions.isLive(live.id))
|
|
259
275
|
return;
|
|
260
|
-
this.
|
|
261
|
-
this.close(why, live.id, endedBy).catch((cause) => this.
|
|
276
|
+
this.log('warning', `session ${live.id} ended: ${why}`);
|
|
277
|
+
this.close(why, live.id, endedBy).catch((cause) => this.log('error', `closing session ${live.id} failed: ${errorMessage(cause)}`));
|
|
262
278
|
}
|
|
263
279
|
/** A recorder writing to `<output>/videos/<session>/`, when the engine records video. */
|
|
264
280
|
recorder(id, config, attempt) {
|
|
@@ -315,7 +331,7 @@ export class SessionHost {
|
|
|
315
331
|
openingText(live, config, screen) {
|
|
316
332
|
const engine = live.target.engine;
|
|
317
333
|
const lines = [
|
|
318
|
-
`Session ${live.id} open on target "${live.target.name}" (platform ${live.target.platform}, engine ${engine === undefined ? 'none' : `${engine.name} ${engine.version}`}), ${
|
|
334
|
+
`Session ${live.id} open on target "${live.target.name}" (platform ${live.target.platform}, engine ${engine === undefined ? 'none' : `${engine.name} ${engine.version}`}), ${live.headed ? 'headed' : 'headless'}; config ${live.configPath}.`,
|
|
319
335
|
];
|
|
320
336
|
if (live.target.app.base !== undefined) {
|
|
321
337
|
lines.push(`App: ${live.target.app.base.href}.`);
|
|
@@ -385,6 +401,10 @@ export class SessionHost {
|
|
|
385
401
|
inputSchema: z.object({
|
|
386
402
|
target: z.string().min(1).optional().describe('Target name from the config; required when the config declares several'),
|
|
387
403
|
config: z.string().min(1).optional().describe("Path to an e2e config file, relative to the server's directory; default: the nearest e2e.config.ts"),
|
|
404
|
+
headed: z
|
|
405
|
+
.boolean()
|
|
406
|
+
.optional()
|
|
407
|
+
.describe('Show the browser or simulator, when the engine supports it; pass true when the user wants to watch. Default: headless, unless the server was started with --headed'),
|
|
388
408
|
}).strict(),
|
|
389
409
|
readOnly: false,
|
|
390
410
|
call: async (args, extra) => textResult(await this.open(args, extra.signal)),
|
|
@@ -427,3 +447,24 @@ export class SessionHost {
|
|
|
427
447
|
});
|
|
428
448
|
}
|
|
429
449
|
}
|
|
450
|
+
/**
|
|
451
|
+
* The tool with what it returns redacted with every secret value the process
|
|
452
|
+
* knows: the static values of each config a session loaded, and every value
|
|
453
|
+
* a session resolved or derived. A failure becomes a result the agent can
|
|
454
|
+
* react to, never a protocol error, and is redacted the same way.
|
|
455
|
+
*/
|
|
456
|
+
function redacting(spec) {
|
|
457
|
+
return {
|
|
458
|
+
...spec,
|
|
459
|
+
call: async (args, extra) => {
|
|
460
|
+
let result;
|
|
461
|
+
try {
|
|
462
|
+
result = await spec.call(args, extra);
|
|
463
|
+
}
|
|
464
|
+
catch (cause) {
|
|
465
|
+
result = errorResult(cause);
|
|
466
|
+
}
|
|
467
|
+
return redactResult(result, (text) => processSecrets.redact(text));
|
|
468
|
+
},
|
|
469
|
+
};
|
|
470
|
+
}
|
package/dist/mcp/tools.js
CHANGED
|
@@ -192,13 +192,15 @@ async function resolveOutput(value) {
|
|
|
192
192
|
* else is JSON, so a structured output is never lost.
|
|
193
193
|
*/
|
|
194
194
|
export async function resultFromOutput(tool, output, input = undefined) {
|
|
195
|
-
if (typeof output === 'string')
|
|
196
|
-
return textResult(output);
|
|
197
195
|
if (tool.toModelOutput !== undefined) {
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
196
|
+
const rendered = await tool.toModelOutput({ toolCallId: 'mcp', input, output });
|
|
197
|
+
const content = contentFromModelOutput(rendered);
|
|
198
|
+
if (content !== undefined) {
|
|
199
|
+
return { content, ...(rendered.type === 'error-text' || rendered.type === 'error-json' ? { isError: true } : {}) };
|
|
200
|
+
}
|
|
201
201
|
}
|
|
202
|
+
if (typeof output === 'string')
|
|
203
|
+
return textResult(output);
|
|
202
204
|
return textResult(output === undefined ? 'Done.' : JSON.stringify(output, null, 2));
|
|
203
205
|
}
|
|
204
206
|
function contentFromModelOutput(output) {
|
package/dist/oauth/chatgpt.js
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { createOpenAI } from '@ai-sdk/openai';
|
|
8
8
|
import { createOAuthFetch } from './fetch.js';
|
|
9
|
-
import { USER_AGENT
|
|
9
|
+
import { USER_AGENT } from '../internal/client-identity.js';
|
|
10
|
+
import { loginHint } from './providers.js';
|
|
10
11
|
import { createCodexProvider } from './providers/openai.js';
|
|
12
|
+
import { withoutServerStorage } from './responses.js';
|
|
11
13
|
import { defaultCredentialStore } from './store.js';
|
|
12
14
|
export function chatgpt(modelId) {
|
|
13
15
|
const fetch = createOAuthFetch(createCodexProvider(), {
|
|
@@ -18,25 +20,3 @@ export function chatgpt(modelId) {
|
|
|
18
20
|
// The key header is removed per request; the value only satisfies the constructor.
|
|
19
21
|
return withoutServerStorage(createOpenAI({ apiKey: 'oauth', fetch, name: 'chatgpt' }).responses(modelId));
|
|
20
22
|
}
|
|
21
|
-
/**
|
|
22
|
-
* Tells the SDK what the Codex backend enforces: nothing is stored server
|
|
23
|
-
* side. Believing storage is on, the SDK refers back to an earlier turn's
|
|
24
|
-
* reasoning by id (`item_reference`), which the backend then cannot find; told
|
|
25
|
-
* it is off, the SDK carries the encrypted reasoning itself.
|
|
26
|
-
*/
|
|
27
|
-
function withoutServerStorage(model) {
|
|
28
|
-
const storeOff = (options) => ({
|
|
29
|
-
...options,
|
|
30
|
-
providerOptions: { ...options.providerOptions, openai: { ...options.providerOptions?.['openai'], store: false } },
|
|
31
|
-
});
|
|
32
|
-
return {
|
|
33
|
-
specificationVersion: model.specificationVersion,
|
|
34
|
-
provider: model.provider,
|
|
35
|
-
modelId: model.modelId,
|
|
36
|
-
get supportedUrls() {
|
|
37
|
-
return model.supportedUrls;
|
|
38
|
-
},
|
|
39
|
-
doGenerate: (options) => model.doGenerate(storeOff(options)),
|
|
40
|
-
doStream: (options) => model.doStream(storeOff(options)),
|
|
41
|
-
};
|
|
42
|
-
}
|
package/dist/oauth/cli.js
CHANGED
|
@@ -121,6 +121,7 @@ async function signedIn() {
|
|
|
121
121
|
const PROVIDER_HINTS = {
|
|
122
122
|
openai: 'ChatGPT Plus/Pro, the Codex sign-in',
|
|
123
123
|
'github-copilot': 'GitHub Copilot: OpenAI, Anthropic, Google, and SpaceXAI models',
|
|
124
|
+
'opencode-console': 'your OpenCode Console workspace',
|
|
124
125
|
spacexai: 'SuperGrok or X Premium+',
|
|
125
126
|
};
|
|
126
127
|
/** A terminal picker over providers; without a terminal the provider has to be named. */
|
package/dist/oauth/copilot.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `copilot('claude-sonnet-5')`: a GitHub Copilot subscription as an AI SDK
|
|
3
|
-
* model. Copilot serves
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* model. Copilot serves most of its models over the OpenAI chat protocol and
|
|
4
|
+
* some only over its Responses API; the instance asks the plan's model
|
|
5
|
+
* listing which on the first call and delegates to the matching model, so
|
|
6
|
+
* construction stays synchronous and touches the network only when a call
|
|
7
|
+
* does. An enterprise login stored by `e2e login` routes to its own host.
|
|
7
8
|
*/
|
|
8
9
|
import type { LanguageModelV4 } from '@ai-sdk/provider';
|
|
9
10
|
export declare function copilot(modelId: string): LanguageModelV4;
|
package/dist/oauth/copilot.js
CHANGED
|
@@ -1,26 +1,99 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `copilot('claude-sonnet-5')`: a GitHub Copilot subscription as an AI SDK
|
|
3
|
-
* model. Copilot serves
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* model. Copilot serves most of its models over the OpenAI chat protocol and
|
|
4
|
+
* some only over its Responses API; the instance asks the plan's model
|
|
5
|
+
* listing which on the first call and delegates to the matching model, so
|
|
6
|
+
* construction stays synchronous and touches the network only when a call
|
|
7
|
+
* does. An enterprise login stored by `e2e login` routes to its own host.
|
|
7
8
|
*/
|
|
8
9
|
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
10
|
+
import { OAuthError } from './errors.js';
|
|
9
11
|
import { createOAuthFetch } from './fetch.js';
|
|
10
|
-
import { USER_AGENT
|
|
11
|
-
import {
|
|
12
|
+
import { USER_AGENT } from '../internal/client-identity.js';
|
|
13
|
+
import { loginHint } from './providers.js';
|
|
14
|
+
import { COPILOT_API_URL, copilotProtocolFor, createCopilotProvider } from './providers/github-copilot.js';
|
|
15
|
+
import { withoutServerStorage } from './responses.js';
|
|
12
16
|
import { defaultCredentialStore } from './store.js';
|
|
17
|
+
/** How long the first call waits on the model listing before it calls over chat and asks again next time. */
|
|
18
|
+
const LISTING_TIMEOUT_MS = 10_000;
|
|
13
19
|
export function copilot(modelId) {
|
|
14
20
|
const fetch = createOAuthFetch(createCopilotProvider(), {
|
|
15
21
|
store: defaultCredentialStore(),
|
|
16
22
|
userAgent: USER_AGENT,
|
|
17
23
|
loginHint: loginHint('github-copilot'),
|
|
18
24
|
});
|
|
19
|
-
|
|
25
|
+
// The key header is removed per request; the value only satisfies the constructor.
|
|
26
|
+
const chat = createOpenAICompatible({
|
|
20
27
|
name: 'github-copilot',
|
|
21
28
|
baseURL: COPILOT_API_URL,
|
|
22
29
|
apiKey: 'oauth',
|
|
23
30
|
fetch,
|
|
24
31
|
includeUsage: true,
|
|
25
32
|
}).chatModel(modelId);
|
|
33
|
+
return protocolDelegate(chat, () => responsesModel(modelId, fetch), () => copilotProtocolFor(modelId, fetch, AbortSignal.timeout(LISTING_TIMEOUT_MS)));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The Responses model for `modelId`. `@ai-sdk/openai` is imported only here,
|
|
37
|
+
* so a project that calls only chat models needs no install of it.
|
|
38
|
+
*/
|
|
39
|
+
async function responsesModel(modelId, fetch) {
|
|
40
|
+
let createOpenAI;
|
|
41
|
+
try {
|
|
42
|
+
({ createOpenAI } = await import('@ai-sdk/openai'));
|
|
43
|
+
}
|
|
44
|
+
catch (cause) {
|
|
45
|
+
throw new OAuthError('MISCONFIGURED', `GitHub Copilot serves ${modelId} only over its Responses API, which copilot() calls through @ai-sdk/openai; install it beside @ai-sdk/openai-compatible`, { cause });
|
|
46
|
+
}
|
|
47
|
+
return withoutServerStorage(createOpenAI({ apiKey: 'oauth', baseURL: COPILOT_API_URL, fetch, name: 'github-copilot' }).responses(modelId));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A model that settles its Copilot protocol on the first call and delegates
|
|
51
|
+
* to the model for it. One lookup serves every concurrent call, under its
|
|
52
|
+
* own timeout, so a caller that gives up never fails the others. Only a
|
|
53
|
+
* listing that was read is remembered: when it could not be read, that call
|
|
54
|
+
* goes over chat, the endpoint most models use, and the next call asks again.
|
|
55
|
+
*/
|
|
56
|
+
function protocolDelegate(chat, responses, lookup) {
|
|
57
|
+
let resolved;
|
|
58
|
+
let pending;
|
|
59
|
+
const settle = () => {
|
|
60
|
+
if (resolved !== undefined)
|
|
61
|
+
return Promise.resolve(resolved);
|
|
62
|
+
pending ??= (async () => {
|
|
63
|
+
const protocol = await lookup();
|
|
64
|
+
if (protocol === undefined)
|
|
65
|
+
return chat;
|
|
66
|
+
resolved = protocol === 'responses' ? await responses() : chat;
|
|
67
|
+
return resolved;
|
|
68
|
+
})().finally(() => {
|
|
69
|
+
pending = undefined;
|
|
70
|
+
});
|
|
71
|
+
return pending;
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
specificationVersion: chat.specificationVersion,
|
|
75
|
+
get provider() {
|
|
76
|
+
return resolved?.provider ?? chat.provider;
|
|
77
|
+
},
|
|
78
|
+
get modelId() {
|
|
79
|
+
return resolved?.modelId ?? chat.modelId;
|
|
80
|
+
},
|
|
81
|
+
get supportedUrls() {
|
|
82
|
+
return resolved?.supportedUrls ?? chat.supportedUrls;
|
|
83
|
+
},
|
|
84
|
+
doGenerate: async (options) => (await untilAborted(settle(), options.abortSignal)).doGenerate(options),
|
|
85
|
+
doStream: async (options) => (await untilAborted(settle(), options.abortSignal)).doStream(options),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Waits for `promise` unless `signal` aborts first, leaving the shared promise running for other callers. */
|
|
89
|
+
function untilAborted(promise, signal) {
|
|
90
|
+
if (signal === undefined)
|
|
91
|
+
return promise;
|
|
92
|
+
if (signal.aborted)
|
|
93
|
+
return Promise.reject(signal.reason);
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
const onAbort = () => reject(signal.reason);
|
|
96
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
97
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener('abort', onAbort));
|
|
98
|
+
});
|
|
26
99
|
}
|
package/dist/oauth/grok.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { createXai } from '@ai-sdk/xai';
|
|
7
7
|
import { createOAuthFetch } from './fetch.js';
|
|
8
|
-
import { USER_AGENT
|
|
8
|
+
import { USER_AGENT } from '../internal/client-identity.js';
|
|
9
|
+
import { loginHint } from './providers.js';
|
|
9
10
|
import { createXaiProvider } from './providers/xai.js';
|
|
10
11
|
import { defaultCredentialStore } from './store.js';
|
|
11
12
|
export function grok(modelId) {
|
package/dist/oauth/models.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** Lists the models a stored subscription login serves, through the login's own fetch. */
|
|
2
2
|
import { OAuthError } from './errors.js';
|
|
3
3
|
import { createOAuthFetch } from './fetch.js';
|
|
4
|
-
import { USER_AGENT
|
|
4
|
+
import { USER_AGENT } from '../internal/client-identity.js';
|
|
5
|
+
import { getProvider, loginHint } from './providers.js';
|
|
5
6
|
import { defaultCredentialStore } from './store.js';
|
|
6
7
|
/** The models `providerId`'s stored login serves; `NOT_LOGGED_IN` without one. */
|
|
7
8
|
export async function listModels(providerId, store = defaultCredentialStore()) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `opencodeConsole('deepseek-v4.1-flash')`: an OpenCode Console workspace as
|
|
3
|
+
* an AI SDK model, Zen for a bare id and Go for a `go/` one. The first call
|
|
4
|
+
* reads the workspace config for the model's protocol and lazily loads the
|
|
5
|
+
* SDK for it. Requests get the prompt-cache hints the runner itself adds for
|
|
6
|
+
* OpenAI and Anthropic, which it cannot do here since the protocol is unknown
|
|
7
|
+
* until then. `OPENCODE_API_KEY` replaces the stored login when set.
|
|
8
|
+
*/
|
|
9
|
+
import type { LanguageModelV4 } from '@ai-sdk/provider';
|
|
10
|
+
export declare function opencodeConsole(modelId: string): LanguageModelV4;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `opencodeConsole('deepseek-v4.1-flash')`: an OpenCode Console workspace as
|
|
3
|
+
* an AI SDK model, Zen for a bare id and Go for a `go/` one. The first call
|
|
4
|
+
* reads the workspace config for the model's protocol and lazily loads the
|
|
5
|
+
* SDK for it. Requests get the prompt-cache hints the runner itself adds for
|
|
6
|
+
* OpenAI and Anthropic, which it cannot do here since the protocol is unknown
|
|
7
|
+
* until then. `OPENCODE_API_KEY` replaces the stored login when set.
|
|
8
|
+
*/
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
11
|
+
import { promptCacheKey } from '../agent/model/provider-hints.js';
|
|
12
|
+
import { OAuthError } from './errors.js';
|
|
13
|
+
import { createOAuthFetch } from './fetch.js';
|
|
14
|
+
import { USER_AGENT } from '../internal/client-identity.js';
|
|
15
|
+
import { loginHint } from './providers.js';
|
|
16
|
+
import { createOpencodeConsoleProvider, opencodeConsoleChatRoute, opencodeConsoleRouteFor } from './providers/opencode-console.js';
|
|
17
|
+
import { EnvCredentialStore, defaultCredentialStore } from './store.js';
|
|
18
|
+
const CONFIG_TIMEOUT_MS = 10_000;
|
|
19
|
+
const API_KEY_ENV = 'OPENCODE_API_KEY';
|
|
20
|
+
const BREAKPOINT = { cacheControl: { type: 'ephemeral' } };
|
|
21
|
+
export function opencodeConsole(modelId) {
|
|
22
|
+
const apiKey = process.env[API_KEY_ENV]?.trim();
|
|
23
|
+
const fetch = createOAuthFetch(createOpencodeConsoleProvider(), {
|
|
24
|
+
store: apiKey ? apiKeyStore(apiKey) : defaultCredentialStore(),
|
|
25
|
+
userAgent: USER_AGENT,
|
|
26
|
+
loginHint: apiKey ? `check ${API_KEY_ENV}` : loginHint('opencode-console'),
|
|
27
|
+
});
|
|
28
|
+
// One per model, so a worker's calls stay on one upstream and its cache.
|
|
29
|
+
const session = `e2e_${randomUUID()}`;
|
|
30
|
+
const chatRoute = opencodeConsoleChatRoute(modelId);
|
|
31
|
+
const chat = { npm: chatRoute.npm, model: chatModel(chatRoute, fetch, session) };
|
|
32
|
+
let resolved;
|
|
33
|
+
let pending;
|
|
34
|
+
// An unreadable config sends that call over chat and is asked again next time.
|
|
35
|
+
const served = () => {
|
|
36
|
+
if (resolved !== undefined)
|
|
37
|
+
return Promise.resolve(resolved);
|
|
38
|
+
pending ??= (async () => {
|
|
39
|
+
const route = await opencodeConsoleRouteFor(modelId, fetch, AbortSignal.timeout(CONFIG_TIMEOUT_MS));
|
|
40
|
+
if (route === undefined)
|
|
41
|
+
return chat;
|
|
42
|
+
resolved = { npm: route.npm, model: await sdkModel(route, fetch, session) };
|
|
43
|
+
return resolved;
|
|
44
|
+
})().finally(() => {
|
|
45
|
+
pending = undefined;
|
|
46
|
+
});
|
|
47
|
+
return pending;
|
|
48
|
+
};
|
|
49
|
+
// Fixed: the runner reads provider and modelId before and after the first call.
|
|
50
|
+
return {
|
|
51
|
+
specificationVersion: chat.model.specificationVersion,
|
|
52
|
+
provider: chatRoute.go ? 'opencode.go' : 'opencode',
|
|
53
|
+
modelId: chatRoute.modelId,
|
|
54
|
+
get supportedUrls() {
|
|
55
|
+
return (resolved ?? chat).model.supportedUrls;
|
|
56
|
+
},
|
|
57
|
+
async doGenerate(options) {
|
|
58
|
+
const { npm, model } = await served();
|
|
59
|
+
return model.doGenerate(requestFor(npm, options, session));
|
|
60
|
+
},
|
|
61
|
+
async doStream(options) {
|
|
62
|
+
const { npm, model } = await served();
|
|
63
|
+
return model.doStream(requestFor(npm, options, session));
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** A Console service API key as a login with nothing to refresh. */
|
|
68
|
+
function apiKeyStore(apiKey) {
|
|
69
|
+
return new EnvCredentialStore(JSON.stringify({ 'opencode-console': { access: apiKey, refresh: '', expires: 0 } }));
|
|
70
|
+
}
|
|
71
|
+
/** `opencode.go` keeps Go's provider options under `opencode`, the name up to the first dot; `apiKey` only satisfies the constructors. */
|
|
72
|
+
function settings(route, fetch, session) {
|
|
73
|
+
return { name: route.go ? 'opencode.go' : 'opencode', baseURL: route.baseURL, apiKey: 'oauth', fetch, headers: { 'x-opencode-session': session } };
|
|
74
|
+
}
|
|
75
|
+
function chatModel(route, fetch, session) {
|
|
76
|
+
return createOpenAICompatible({ ...settings(route, fetch, session), includeUsage: true }).chatModel(route.modelId);
|
|
77
|
+
}
|
|
78
|
+
async function sdkModel(route, fetch, session) {
|
|
79
|
+
switch (route.npm) {
|
|
80
|
+
case '@ai-sdk/openai': {
|
|
81
|
+
const { createOpenAI } = await load(() => import('@ai-sdk/openai'), route);
|
|
82
|
+
return createOpenAI(settings(route, fetch, session)).responses(route.modelId);
|
|
83
|
+
}
|
|
84
|
+
case '@ai-sdk/anthropic': {
|
|
85
|
+
const { createAnthropic } = await load(() => import('@ai-sdk/anthropic'), route);
|
|
86
|
+
return createAnthropic(settings(route, fetch, session))(route.modelId);
|
|
87
|
+
}
|
|
88
|
+
case '@ai-sdk/google': {
|
|
89
|
+
const { createGoogleGenerativeAI } = await load(() => import('@ai-sdk/google'), route);
|
|
90
|
+
return createGoogleGenerativeAI(settings(route, fetch, session))(route.modelId);
|
|
91
|
+
}
|
|
92
|
+
default:
|
|
93
|
+
return chatModel(route, fetch, session);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Responses: no server storage and the runner's prompt cache key. Anthropic: cache breakpoints. */
|
|
97
|
+
function requestFor(npm, options, session) {
|
|
98
|
+
switch (npm) {
|
|
99
|
+
case '@ai-sdk/openai': {
|
|
100
|
+
const system = options.prompt.flatMap((message) => (message.role === 'system' ? [message.content] : [])).join('\n');
|
|
101
|
+
const cacheKey = system === '' ? session : promptCacheKey(system);
|
|
102
|
+
return { ...options, providerOptions: { ...options.providerOptions, openai: { promptCacheKey: cacheKey, ...options.providerOptions?.['openai'], store: false } } };
|
|
103
|
+
}
|
|
104
|
+
case '@ai-sdk/anthropic':
|
|
105
|
+
return { ...options, prompt: withBreakpoints(options.prompt, (options.tools?.length ?? 0) > 0) };
|
|
106
|
+
default:
|
|
107
|
+
return options;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The system prompt, and in a tool loop the newest message, as the runner
|
|
112
|
+
* marks them; a call without tools is a one-off judgment. A prompt that already
|
|
113
|
+
* carries a breakpoint is left as the caller placed it.
|
|
114
|
+
*/
|
|
115
|
+
function withBreakpoints(prompt, loop) {
|
|
116
|
+
if (prompt.some(hasBreakpoint))
|
|
117
|
+
return prompt;
|
|
118
|
+
const marked = new Set([prompt.findLastIndex((message) => message.role === 'system'), loop ? prompt.length - 1 : -1]);
|
|
119
|
+
return prompt.map((message, index) => marked.has(index) ? { ...message, providerOptions: { ...message.providerOptions, anthropic: { ...message.providerOptions?.['anthropic'], ...BREAKPOINT } } } : message);
|
|
120
|
+
}
|
|
121
|
+
function hasBreakpoint(message) {
|
|
122
|
+
const marked = (providerOptions) => providerOptions?.['anthropic']?.['cacheControl'] !== undefined;
|
|
123
|
+
return marked(message.providerOptions) || (Array.isArray(message.content) && message.content.some((part) => marked(part.providerOptions)));
|
|
124
|
+
}
|
|
125
|
+
/** Names the missing package when an SDK is not installed. */
|
|
126
|
+
async function load(importer, route) {
|
|
127
|
+
try {
|
|
128
|
+
return await importer();
|
|
129
|
+
}
|
|
130
|
+
catch (cause) {
|
|
131
|
+
throw new OAuthError('MISCONFIGURED', `OpenCode Console serves ${route.modelId} through ${route.npm}, which opencodeConsole() loads on demand; install it beside @ai-sdk/openai-compatible`, { cause });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -36,3 +36,15 @@ export declare function copilotBaseUrl(enterpriseUrl?: string): string;
|
|
|
36
36
|
export declare function createCopilotProvider(options?: CopilotProviderOptions): OAuthProvider<CopilotCredentials, CopilotLoginOptions>;
|
|
37
37
|
/** Routes an enterprise login at its host and adds Copilot's headers: who initiated the turn, and that images are present. */
|
|
38
38
|
export declare function sendCopilotRequest(request: Request, credentials: CopilotCredentials, upstream: FetchFunction): Promise<Response>;
|
|
39
|
+
/** The endpoint `copilot()` reaches a Copilot chat model over: chat completions or the Responses API. */
|
|
40
|
+
export type CopilotProtocol = 'chat' | 'responses';
|
|
41
|
+
/**
|
|
42
|
+
* Which endpoint `copilot()` reaches `modelId` on, read from the Copilot
|
|
43
|
+
* API's `GET /models`, or `undefined` when the listing could not be read
|
|
44
|
+
* (an error status, a body that is not JSON, the network, the signal), so
|
|
45
|
+
* the caller can ask again instead of remembering a guess. A model the
|
|
46
|
+
* listing does not name, or names with no endpoint `copilot()` speaks, is
|
|
47
|
+
* chat: the vendor's own error then explains it. A rejected login is not a
|
|
48
|
+
* missing listing and rejects.
|
|
49
|
+
*/
|
|
50
|
+
export declare function copilotProtocolFor(modelId: string, fetch: FetchFunction, signal: AbortSignal): Promise<CopilotProtocol | undefined>;
|