fix(coding-agent,ai): restore Bun binary lazy provider loading closes #2314
This commit is contained in:
@@ -34,21 +34,6 @@ interface LazyProviderModule<
|
|||||||
) => AsyncIterable<AssistantMessageEvent>;
|
) => AsyncIterable<AssistantMessageEvent>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type DynamicImport = (specifier: string) => Promise<unknown>;
|
|
||||||
|
|
||||||
const dynamicImport: DynamicImport = (specifier) => import(specifier);
|
|
||||||
|
|
||||||
const ANTHROPIC_PROVIDER_SPECIFIER = "./anthropic.js";
|
|
||||||
const AZURE_OPENAI_RESPONSES_PROVIDER_SPECIFIER = "./azure-openai-responses.js";
|
|
||||||
const GOOGLE_PROVIDER_SPECIFIER = "./google.js";
|
|
||||||
const GOOGLE_GEMINI_CLI_PROVIDER_SPECIFIER = "./google-gemini-cli.js";
|
|
||||||
const GOOGLE_VERTEX_PROVIDER_SPECIFIER = "./google-vertex.js";
|
|
||||||
const MISTRAL_PROVIDER_SPECIFIER = "./mistral.js";
|
|
||||||
const OPENAI_CODEX_RESPONSES_PROVIDER_SPECIFIER = "./openai-codex-responses.js";
|
|
||||||
const OPENAI_COMPLETIONS_PROVIDER_SPECIFIER = "./openai-completions.js";
|
|
||||||
const OPENAI_RESPONSES_PROVIDER_SPECIFIER = "./openai-responses.js";
|
|
||||||
const BEDROCK_PROVIDER_SPECIFIER = "./amazon-" + "bedrock.js";
|
|
||||||
|
|
||||||
interface AnthropicProviderModule {
|
interface AnthropicProviderModule {
|
||||||
streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOptions>;
|
streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOptions>;
|
||||||
streamSimpleAnthropic: StreamFunction<"anthropic-messages", SimpleStreamOptions>;
|
streamSimpleAnthropic: StreamFunction<"anthropic-messages", SimpleStreamOptions>;
|
||||||
@@ -107,6 +92,8 @@ interface BedrockProviderModule {
|
|||||||
) => AsyncIterable<AssistantMessageEvent>;
|
) => AsyncIterable<AssistantMessageEvent>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const importNodeOnlyProvider = (specifier: string): Promise<unknown> => import(specifier);
|
||||||
|
|
||||||
let anthropicProviderModulePromise:
|
let anthropicProviderModulePromise:
|
||||||
| Promise<LazyProviderModule<"anthropic-messages", AnthropicOptions, SimpleStreamOptions>>
|
| Promise<LazyProviderModule<"anthropic-messages", AnthropicOptions, SimpleStreamOptions>>
|
||||||
| undefined;
|
| undefined;
|
||||||
@@ -225,7 +212,7 @@ function createLazySimpleStream<
|
|||||||
function loadAnthropicProviderModule(): Promise<
|
function loadAnthropicProviderModule(): Promise<
|
||||||
LazyProviderModule<"anthropic-messages", AnthropicOptions, SimpleStreamOptions>
|
LazyProviderModule<"anthropic-messages", AnthropicOptions, SimpleStreamOptions>
|
||||||
> {
|
> {
|
||||||
anthropicProviderModulePromise ||= dynamicImport(ANTHROPIC_PROVIDER_SPECIFIER).then((module) => {
|
anthropicProviderModulePromise ||= import("./anthropic.js").then((module) => {
|
||||||
const provider = module as AnthropicProviderModule;
|
const provider = module as AnthropicProviderModule;
|
||||||
return {
|
return {
|
||||||
stream: provider.streamAnthropic,
|
stream: provider.streamAnthropic,
|
||||||
@@ -238,22 +225,20 @@ function loadAnthropicProviderModule(): Promise<
|
|||||||
function loadAzureOpenAIResponsesProviderModule(): Promise<
|
function loadAzureOpenAIResponsesProviderModule(): Promise<
|
||||||
LazyProviderModule<"azure-openai-responses", AzureOpenAIResponsesOptions, SimpleStreamOptions>
|
LazyProviderModule<"azure-openai-responses", AzureOpenAIResponsesOptions, SimpleStreamOptions>
|
||||||
> {
|
> {
|
||||||
azureOpenAIResponsesProviderModulePromise ||= dynamicImport(AZURE_OPENAI_RESPONSES_PROVIDER_SPECIFIER).then(
|
azureOpenAIResponsesProviderModulePromise ||= import("./azure-openai-responses.js").then((module) => {
|
||||||
(module) => {
|
const provider = module as AzureOpenAIResponsesProviderModule;
|
||||||
const provider = module as AzureOpenAIResponsesProviderModule;
|
return {
|
||||||
return {
|
stream: provider.streamAzureOpenAIResponses,
|
||||||
stream: provider.streamAzureOpenAIResponses,
|
streamSimple: provider.streamSimpleAzureOpenAIResponses,
|
||||||
streamSimple: provider.streamSimpleAzureOpenAIResponses,
|
};
|
||||||
};
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
return azureOpenAIResponsesProviderModulePromise;
|
return azureOpenAIResponsesProviderModulePromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadGoogleProviderModule(): Promise<
|
function loadGoogleProviderModule(): Promise<
|
||||||
LazyProviderModule<"google-generative-ai", GoogleOptions, SimpleStreamOptions>
|
LazyProviderModule<"google-generative-ai", GoogleOptions, SimpleStreamOptions>
|
||||||
> {
|
> {
|
||||||
googleProviderModulePromise ||= dynamicImport(GOOGLE_PROVIDER_SPECIFIER).then((module) => {
|
googleProviderModulePromise ||= import("./google.js").then((module) => {
|
||||||
const provider = module as GoogleProviderModule;
|
const provider = module as GoogleProviderModule;
|
||||||
return {
|
return {
|
||||||
stream: provider.streamGoogle,
|
stream: provider.streamGoogle,
|
||||||
@@ -266,7 +251,7 @@ function loadGoogleProviderModule(): Promise<
|
|||||||
function loadGoogleGeminiCliProviderModule(): Promise<
|
function loadGoogleGeminiCliProviderModule(): Promise<
|
||||||
LazyProviderModule<"google-gemini-cli", GoogleGeminiCliOptions, SimpleStreamOptions>
|
LazyProviderModule<"google-gemini-cli", GoogleGeminiCliOptions, SimpleStreamOptions>
|
||||||
> {
|
> {
|
||||||
googleGeminiCliProviderModulePromise ||= dynamicImport(GOOGLE_GEMINI_CLI_PROVIDER_SPECIFIER).then((module) => {
|
googleGeminiCliProviderModulePromise ||= import("./google-gemini-cli.js").then((module) => {
|
||||||
const provider = module as GoogleGeminiCliProviderModule;
|
const provider = module as GoogleGeminiCliProviderModule;
|
||||||
return {
|
return {
|
||||||
stream: provider.streamGoogleGeminiCli,
|
stream: provider.streamGoogleGeminiCli,
|
||||||
@@ -279,7 +264,7 @@ function loadGoogleGeminiCliProviderModule(): Promise<
|
|||||||
function loadGoogleVertexProviderModule(): Promise<
|
function loadGoogleVertexProviderModule(): Promise<
|
||||||
LazyProviderModule<"google-vertex", GoogleVertexOptions, SimpleStreamOptions>
|
LazyProviderModule<"google-vertex", GoogleVertexOptions, SimpleStreamOptions>
|
||||||
> {
|
> {
|
||||||
googleVertexProviderModulePromise ||= dynamicImport(GOOGLE_VERTEX_PROVIDER_SPECIFIER).then((module) => {
|
googleVertexProviderModulePromise ||= import("./google-vertex.js").then((module) => {
|
||||||
const provider = module as GoogleVertexProviderModule;
|
const provider = module as GoogleVertexProviderModule;
|
||||||
return {
|
return {
|
||||||
stream: provider.streamGoogleVertex,
|
stream: provider.streamGoogleVertex,
|
||||||
@@ -292,7 +277,7 @@ function loadGoogleVertexProviderModule(): Promise<
|
|||||||
function loadMistralProviderModule(): Promise<
|
function loadMistralProviderModule(): Promise<
|
||||||
LazyProviderModule<"mistral-conversations", MistralOptions, SimpleStreamOptions>
|
LazyProviderModule<"mistral-conversations", MistralOptions, SimpleStreamOptions>
|
||||||
> {
|
> {
|
||||||
mistralProviderModulePromise ||= dynamicImport(MISTRAL_PROVIDER_SPECIFIER).then((module) => {
|
mistralProviderModulePromise ||= import("./mistral.js").then((module) => {
|
||||||
const provider = module as MistralProviderModule;
|
const provider = module as MistralProviderModule;
|
||||||
return {
|
return {
|
||||||
stream: provider.streamMistral,
|
stream: provider.streamMistral,
|
||||||
@@ -305,22 +290,20 @@ function loadMistralProviderModule(): Promise<
|
|||||||
function loadOpenAICodexResponsesProviderModule(): Promise<
|
function loadOpenAICodexResponsesProviderModule(): Promise<
|
||||||
LazyProviderModule<"openai-codex-responses", OpenAICodexResponsesOptions, SimpleStreamOptions>
|
LazyProviderModule<"openai-codex-responses", OpenAICodexResponsesOptions, SimpleStreamOptions>
|
||||||
> {
|
> {
|
||||||
openAICodexResponsesProviderModulePromise ||= dynamicImport(OPENAI_CODEX_RESPONSES_PROVIDER_SPECIFIER).then(
|
openAICodexResponsesProviderModulePromise ||= import("./openai-codex-responses.js").then((module) => {
|
||||||
(module) => {
|
const provider = module as OpenAICodexResponsesProviderModule;
|
||||||
const provider = module as OpenAICodexResponsesProviderModule;
|
return {
|
||||||
return {
|
stream: provider.streamOpenAICodexResponses,
|
||||||
stream: provider.streamOpenAICodexResponses,
|
streamSimple: provider.streamSimpleOpenAICodexResponses,
|
||||||
streamSimple: provider.streamSimpleOpenAICodexResponses,
|
};
|
||||||
};
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
return openAICodexResponsesProviderModulePromise;
|
return openAICodexResponsesProviderModulePromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadOpenAICompletionsProviderModule(): Promise<
|
function loadOpenAICompletionsProviderModule(): Promise<
|
||||||
LazyProviderModule<"openai-completions", OpenAICompletionsOptions, SimpleStreamOptions>
|
LazyProviderModule<"openai-completions", OpenAICompletionsOptions, SimpleStreamOptions>
|
||||||
> {
|
> {
|
||||||
openAICompletionsProviderModulePromise ||= dynamicImport(OPENAI_COMPLETIONS_PROVIDER_SPECIFIER).then((module) => {
|
openAICompletionsProviderModulePromise ||= import("./openai-completions.js").then((module) => {
|
||||||
const provider = module as OpenAICompletionsProviderModule;
|
const provider = module as OpenAICompletionsProviderModule;
|
||||||
return {
|
return {
|
||||||
stream: provider.streamOpenAICompletions,
|
stream: provider.streamOpenAICompletions,
|
||||||
@@ -333,7 +316,7 @@ function loadOpenAICompletionsProviderModule(): Promise<
|
|||||||
function loadOpenAIResponsesProviderModule(): Promise<
|
function loadOpenAIResponsesProviderModule(): Promise<
|
||||||
LazyProviderModule<"openai-responses", OpenAIResponsesOptions, SimpleStreamOptions>
|
LazyProviderModule<"openai-responses", OpenAIResponsesOptions, SimpleStreamOptions>
|
||||||
> {
|
> {
|
||||||
openAIResponsesProviderModulePromise ||= dynamicImport(OPENAI_RESPONSES_PROVIDER_SPECIFIER).then((module) => {
|
openAIResponsesProviderModulePromise ||= import("./openai-responses.js").then((module) => {
|
||||||
const provider = module as OpenAIResponsesProviderModule;
|
const provider = module as OpenAIResponsesProviderModule;
|
||||||
return {
|
return {
|
||||||
stream: provider.streamOpenAIResponses,
|
stream: provider.streamOpenAIResponses,
|
||||||
@@ -349,7 +332,7 @@ function loadBedrockProviderModule(): Promise<
|
|||||||
if (bedrockProviderModuleOverride) {
|
if (bedrockProviderModuleOverride) {
|
||||||
return Promise.resolve(bedrockProviderModuleOverride);
|
return Promise.resolve(bedrockProviderModuleOverride);
|
||||||
}
|
}
|
||||||
bedrockProviderModulePromise ||= dynamicImport(BEDROCK_PROVIDER_SPECIFIER).then((module) => {
|
bedrockProviderModulePromise ||= importNodeOnlyProvider("./amazon-bedrock.js").then((module) => {
|
||||||
const provider = module as BedrockProviderModule;
|
const provider = module as BedrockProviderModule;
|
||||||
return {
|
return {
|
||||||
stream: provider.streamBedrock,
|
stream: provider.streamBedrock,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"clean": "shx rm -rf dist",
|
"clean": "shx rm -rf dist",
|
||||||
"dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput",
|
"dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput",
|
||||||
"build": "tsgo -p tsconfig.build.json && shx chmod +x dist/cli.js && npm run copy-assets",
|
"build": "tsgo -p tsconfig.build.json && shx chmod +x dist/cli.js && npm run copy-assets",
|
||||||
"build:binary": "npm --prefix ../tui run build && npm --prefix ../ai run build && npm --prefix ../agent run build && npm run build && bun build --compile ./dist/cli.js --outfile dist/pi && npm run copy-binary-assets",
|
"build:binary": "npm --prefix ../tui run build && npm --prefix ../ai run build && npm --prefix ../agent run build && npm run build && bun build --compile ./dist/bun/cli.js --outfile dist/pi && npm run copy-binary-assets",
|
||||||
"copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/",
|
"copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/",
|
||||||
"copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/export-html/template.html dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx cp -r docs dist/ && shx cp -r examples dist/ && shx cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm dist/",
|
"copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/export-html/template.html dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx cp -r docs dist/ && shx cp -r examples dist/ && shx cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm dist/",
|
||||||
"test": "vitest --run",
|
"test": "vitest --run",
|
||||||
|
|||||||
5
packages/coding-agent/src/bun/cli.ts
Normal file
5
packages/coding-agent/src/bun/cli.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
process.title = "pi";
|
||||||
|
|
||||||
|
await import("./register-bedrock.js");
|
||||||
|
await import("../cli.js");
|
||||||
118
packages/coding-agent/src/bun/register-bedrock.ts
Normal file
118
packages/coding-agent/src/bun/register-bedrock.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import {
|
||||||
|
type AssistantMessage,
|
||||||
|
type AssistantMessageEvent,
|
||||||
|
createAssistantMessageEventStream,
|
||||||
|
type Model,
|
||||||
|
registerApiProvider,
|
||||||
|
type SimpleStreamOptions,
|
||||||
|
type StreamFunction,
|
||||||
|
type StreamOptions,
|
||||||
|
} from "@mariozechner/pi-ai";
|
||||||
|
|
||||||
|
interface LazyBedrockProviderModule {
|
||||||
|
streamBedrock: StreamFunction<"bedrock-converse-stream", StreamOptions>;
|
||||||
|
streamSimpleBedrock: StreamFunction<"bedrock-converse-stream", SimpleStreamOptions>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let bedrockProviderModulePromise: Promise<LazyBedrockProviderModule> | undefined;
|
||||||
|
|
||||||
|
function forwardStream(
|
||||||
|
target: ReturnType<typeof createAssistantMessageEventStream>,
|
||||||
|
source: AsyncIterable<AssistantMessageEvent>,
|
||||||
|
): void {
|
||||||
|
(async () => {
|
||||||
|
for await (const event of source) {
|
||||||
|
target.push(event);
|
||||||
|
}
|
||||||
|
target.end();
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLazyLoadErrorMessage(model: Model<"bedrock-converse-stream">, error: unknown): AssistantMessage {
|
||||||
|
return {
|
||||||
|
role: "assistant",
|
||||||
|
content: [],
|
||||||
|
api: model.api,
|
||||||
|
provider: model.provider,
|
||||||
|
model: model.id,
|
||||||
|
usage: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
totalTokens: 0,
|
||||||
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||||
|
},
|
||||||
|
stopReason: "error",
|
||||||
|
errorMessage: error instanceof Error ? error.message : String(error),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadBedrockProviderModule(): Promise<LazyBedrockProviderModule> {
|
||||||
|
if (!bedrockProviderModulePromise) {
|
||||||
|
bedrockProviderModulePromise = import("@mariozechner/pi-ai/bedrock-provider").then(
|
||||||
|
({ bedrockProviderModule }) => {
|
||||||
|
return {
|
||||||
|
streamBedrock: (model, context, options) => {
|
||||||
|
const outer = createAssistantMessageEventStream();
|
||||||
|
const inner = bedrockProviderModule.streamBedrock(model, context, options);
|
||||||
|
forwardStream(outer, inner);
|
||||||
|
return outer;
|
||||||
|
},
|
||||||
|
streamSimpleBedrock: (model, context, options) => {
|
||||||
|
const outer = createAssistantMessageEventStream();
|
||||||
|
const inner = bedrockProviderModule.streamSimpleBedrock(model, context, options);
|
||||||
|
forwardStream(outer, inner);
|
||||||
|
return outer;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return bedrockProviderModulePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
const streamBedrockLazy: StreamFunction<"bedrock-converse-stream", StreamOptions> = (model, context, options) => {
|
||||||
|
const outer = createAssistantMessageEventStream();
|
||||||
|
|
||||||
|
loadBedrockProviderModule()
|
||||||
|
.then((module) => {
|
||||||
|
const inner = module.streamBedrock(model, context, options);
|
||||||
|
forwardStream(outer, inner);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const message = createLazyLoadErrorMessage(model, error);
|
||||||
|
outer.push({ type: "error", reason: "error", error: message });
|
||||||
|
outer.end(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
return outer;
|
||||||
|
};
|
||||||
|
|
||||||
|
const streamSimpleBedrockLazy: StreamFunction<"bedrock-converse-stream", SimpleStreamOptions> = (
|
||||||
|
model,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
) => {
|
||||||
|
const outer = createAssistantMessageEventStream();
|
||||||
|
|
||||||
|
loadBedrockProviderModule()
|
||||||
|
.then((module) => {
|
||||||
|
const inner = module.streamSimpleBedrock(model, context, options);
|
||||||
|
forwardStream(outer, inner);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const message = createLazyLoadErrorMessage(model, error);
|
||||||
|
outer.push({ type: "error", reason: "error", error: message });
|
||||||
|
outer.end(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
return outer;
|
||||||
|
};
|
||||||
|
|
||||||
|
registerApiProvider({
|
||||||
|
api: "bedrock-converse-stream",
|
||||||
|
stream: streamBedrockLazy,
|
||||||
|
streamSimple: streamSimpleBedrockLazy,
|
||||||
|
});
|
||||||
@@ -7,12 +7,9 @@
|
|||||||
*/
|
*/
|
||||||
process.title = "pi";
|
process.title = "pi";
|
||||||
|
|
||||||
import { setBedrockProviderModule } from "@mariozechner/pi-ai";
|
|
||||||
import { bedrockProviderModule } from "@mariozechner/pi-ai/bedrock-provider";
|
|
||||||
import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
|
import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
|
||||||
import { main } from "./main.js";
|
import { main } from "./main.js";
|
||||||
|
|
||||||
setGlobalDispatcher(new EnvHttpProxyAgent());
|
setGlobalDispatcher(new EnvHttpProxyAgent());
|
||||||
setBedrockProviderModule(bedrockProviderModule);
|
|
||||||
|
|
||||||
main(process.argv.slice(2));
|
main(process.argv.slice(2));
|
||||||
|
|||||||
@@ -107,9 +107,9 @@ for platform in "${PLATFORMS[@]}"; do
|
|||||||
# call site has a try/catch fallback. For Windows builds, we copy the
|
# call site has a try/catch fallback. For Windows builds, we copy the
|
||||||
# appropriate .node file alongside the binary below.
|
# appropriate .node file alongside the binary below.
|
||||||
if [[ "$platform" == "windows-x64" ]]; then
|
if [[ "$platform" == "windows-x64" ]]; then
|
||||||
bun build --compile --external koffi --target=bun-$platform ./dist/cli.js --outfile binaries/$platform/pi.exe
|
bun build --compile --external koffi --target=bun-$platform ./dist/bun/cli.js --outfile binaries/$platform/pi.exe
|
||||||
else
|
else
|
||||||
bun build --compile --external koffi --target=bun-$platform ./dist/cli.js --outfile binaries/$platform/pi
|
bun build --compile --external koffi --target=bun-$platform ./dist/bun/cli.js --outfile binaries/$platform/pi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user