fix(ai/providers): added Bedrock proxy support and HTTP/1 fallback retry

- Enabled Bedrock runtime and AWS credential calls to use a proxy-aware HTTP/1 handler when HTTPS_PROXY, HTTP_PROXY, or ALL_PROXY (including lowercase variants) is configured.
- Added a retry path that recreates the Bedrock client with HTTP/1 transport when an initial HTTP/2-related error occurs before streaming starts.
- Updated package and lock dependencies to include Bedrock credential-provider and proxy-agent support, and documented the new Bedrock proxy environment variables.
This commit is contained in:
can1357
2026-04-24 00:55:32 +02:00
parent 6b8b46c410
commit ca875ad5f9
6 changed files with 120 additions and 36 deletions
+7 -3
View File
@@ -36,6 +36,7 @@
"dependencies": {
"@anthropic-ai/sdk": "catalog:",
"@aws-sdk/client-bedrock-runtime": "catalog:",
"@aws-sdk/credential-provider-node": "catalog:",
"@bufbuild/protobuf": "catalog:",
"@google/genai": "catalog:",
"@oh-my-pi/pi-natives": "catalog:",
@@ -46,6 +47,7 @@
"ajv-formats": "catalog:",
"openai": "catalog:",
"partial-json": "catalog:",
"proxy-agent": "catalog:",
"zod": "catalog:",
},
"devDependencies": {
@@ -194,6 +196,7 @@
"@agentclientprotocol/sdk": "0.16.1",
"@anthropic-ai/sdk": "^0.78",
"@aws-sdk/client-bedrock-runtime": "^3",
"@aws-sdk/credential-provider-node": "^3",
"@babel/generator": "^7.29",
"@babel/parser": "^7.29",
"@babel/traverse": "^7.29",
@@ -241,6 +244,7 @@
"partial-json": "^0.1",
"postcss": "^8.5",
"prettier": "^3.8",
"proxy-agent": "^6.5",
"puppeteer": "^24.37",
"react": "^19.2",
"react-chartjs-2": "^5.3",
@@ -845,7 +849,7 @@
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
"data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="],
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
@@ -1337,14 +1341,14 @@
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"get-uri/data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="],
"js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"jszip/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"log-update/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
"node-fetch/data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
"proxy-agent/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
"rss-parser/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="],
+2
View File
@@ -117,6 +117,8 @@ When `CLAUDE_CODE_USE_FOUNDRY` is enabled, Anthropic requests switch to Foundry
| `AWS_WEB_IDENTITY_TOKEN_FILE` + `AWS_ROLE_ARN` | Enables web identity auth path |
| `AWS_BEDROCK_SKIP_AUTH` | If `1`, injects dummy credentials (proxy/non-auth scenarios) |
| `AWS_BEDROCK_FORCE_HTTP1` | If `1`, forces Node HTTP/1 request handler |
| `HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY` | Routes Bedrock runtime and AWS SSO credential calls through the configured proxy using HTTP/1 |
| `NO_PROXY` | Excludes matching hosts from proxy routing when a proxy variable is configured |
Region fallback in provider code: `options.region` → `AWS_REGION` → `AWS_DEFAULT_REGION` → `us-east-1`.
+3
View File
@@ -11,6 +11,7 @@
"@agentclientprotocol/sdk": "0.16.1",
"@anthropic-ai/sdk": "^0.78",
"@aws-sdk/client-bedrock-runtime": "^3",
"@aws-sdk/credential-provider-node": "^3",
"@babel/generator": "^7.29",
"@babel/parser": "^7.29",
"@babel/traverse": "^7.29",
@@ -58,6 +59,7 @@
"partial-json": "^0.1",
"postcss": "^8.5",
"prettier": "^3.8",
"proxy-agent": "^6.5",
"puppeteer": "^24.37",
"react": "^19.2",
"react-chartjs-2": "^5.3",
@@ -105,6 +107,7 @@
"ci:test:install-methods": "bash scripts/install-tests/run-ci.sh",
"ci:release:verify-natives": "bun scripts/ci-release-verify-natives.ts",
"ci:release:build-binaries": "bun scripts/ci-release-build-binaries.ts",
"ci:release:build-archives": "bun scripts/ci-release-build-archives.ts",
"ci:release:publish": "bun scripts/ci-release-publish.ts",
"bench:gen-fixtures": "bun --cwd=packages/typescript-edit-benchmark run src/generate.ts --typescript-dir /tmp/typescript-source --count-per-type 8",
"bench:edit": "bun --cwd=packages/typescript-edit-benchmark run start",
+4
View File
@@ -1,6 +1,7 @@
# Changelog
## [Unreleased]
### Added
- Added `gpt-5.5` to the built-in model catalog for both OpenAI Responses (`openai`) and local `litellm` (`openai-completions`) providers
@@ -16,6 +17,9 @@
### Fixed
- Fixed Amazon Bedrock proxy handling to honor lowercase `http_proxy`, `https_proxy`, and `all_proxy` environment variables when using HTTP/1 fallback
- Fixed Amazon Bedrock streaming behind corporate HTTP proxies by using a proxy-aware HTTP/1 transport when `HTTPS_PROXY`, `HTTP_PROXY`, or `ALL_PROXY` is configured, including AWS SSO credential calls.
- Fixed Amazon Bedrock requests to retry once with HTTP/1 when the AWS SDK's default HTTP/2 transport fails before streaming begins.
- Fixed OpenAI Responses streaming to display thinking tokens from local providers (llama.cpp, etc.) that send raw `reasoning_text.delta` events and empty `summary` arrays in `output_item.done`. Previously, thinking content was silently dropped during streaming while non-streaming mode worked correctly.
- Synced the bundled OpenCode Go catalog with the current docs so `kimi-k2.6`, `mimo-v2.5`, and `mimo-v2.5-pro` appear in offline/default model lists.
+2
View File
@@ -43,6 +43,7 @@
"dependencies": {
"@anthropic-ai/sdk": "catalog:",
"@aws-sdk/client-bedrock-runtime": "catalog:",
"@aws-sdk/credential-provider-node": "catalog:",
"@bufbuild/protobuf": "catalog:",
"@google/genai": "catalog:",
"@oh-my-pi/pi-natives": "catalog:",
@@ -53,6 +54,7 @@
"ajv-formats": "catalog:",
"openai": "catalog:",
"partial-json": "catalog:",
"proxy-agent": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
+102 -33
View File
@@ -19,8 +19,10 @@ import {
type ToolConfiguration,
ToolResultStatus,
} from "@aws-sdk/client-bedrock-runtime";
import { type DefaultProviderInit, defaultProvider } from "@aws-sdk/credential-provider-node";
import { $env, $flag } from "@oh-my-pi/pi-utils";
import { NodeHttpHandler } from "@smithy/node-http-handler";
import { ProxyAgent } from "proxy-agent";
import type { Effort } from "../model-thinking";
import { mapEffortToAnthropicAdaptiveEffort, requireSupportedEffort } from "../model-thinking";
import { calculateCost } from "../models";
@@ -60,6 +62,51 @@ export interface BedrockOptions extends StreamOptions {
type Block = (TextContent | ThinkingContent | ToolCall) & { index?: number; partialJson?: string };
const BEDROCK_PROXY_ENV_KEYS = ["HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY", "https_proxy", "http_proxy", "all_proxy"];
function hasBedrockProxyEnvironment(): boolean {
return BEDROCK_PROXY_ENV_KEYS.some(key => Boolean($env[key]?.trim()));
}
function installBedrockHttp1Transport(config: BedrockRuntimeClientConfig): void {
const requestHandler = createBedrockHttp1RequestHandler();
config.requestHandler = requestHandler;
if (hasBedrockProxyEnvironment()) {
config.credentialDefaultProvider = createBedrockCredentialDefaultProvider(requestHandler);
}
}
function createBedrockHttp1RequestHandler(): NodeHttpHandler {
if (!hasBedrockProxyEnvironment()) {
return new NodeHttpHandler();
}
const agent = new ProxyAgent();
return new NodeHttpHandler({
httpAgent: agent,
httpsAgent: agent,
});
}
function createBedrockCredentialDefaultProvider(
requestHandler: NodeHttpHandler,
): NonNullable<BedrockRuntimeClientConfig["credentialDefaultProvider"]> {
return (init?: DefaultProviderInit) =>
defaultProvider({
...init,
clientConfig: {
...init?.clientConfig,
requestHandler,
},
});
}
function isHttp2ResponseError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error);
return /\bhttp2\b|http\/2/i.test(message);
}
export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
model: Model<"bedrock-converse-stream">,
context: Context,
@@ -96,6 +143,8 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
region: options.region,
profile: options.profile,
};
let usesHttp1RequestHandler = false;
let messageStarted = false;
// in Node.js/Bun environment only
if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
@@ -109,16 +158,15 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
};
}
if ($flag("AWS_BEDROCK_FORCE_HTTP1")) {
config.requestHandler = new NodeHttpHandler();
if ($flag("AWS_BEDROCK_FORCE_HTTP1") || hasBedrockProxyEnvironment()) {
usesHttp1RequestHandler = true;
installBedrockHttp1Transport(config);
}
}
config.region = config.region || "us-east-1";
try {
const client = new BedrockRuntimeClient(config);
const cacheRetention = resolveCacheRetention(options.cacheRetention);
const toolConfig = convertToolConfig(context.tools, options.toolChoice);
@@ -150,38 +198,59 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
url: `https://bedrock-runtime.${config.region}.amazonaws.com/model/${model.id}/converse-stream`,
body: commandInput,
};
const command = new ConverseStreamCommand(commandInput);
const response = await client.send(command, { abortSignal: options.signal });
while (true) {
const client = new BedrockRuntimeClient(config);
try {
const command = new ConverseStreamCommand(commandInput);
const response = await client.send(command, { abortSignal: options.signal });
for await (const item of response.stream!) {
if (item.messageStart) {
if (item.messageStart.role !== ConversationRole.ASSISTANT) {
throw new Error("Unexpected assistant message start but got user message start instead");
for await (const item of response.stream!) {
if (item.messageStart) {
messageStarted = true;
if (item.messageStart.role !== ConversationRole.ASSISTANT) {
throw new Error("Unexpected assistant message start but got user message start instead");
}
stream.push({ type: "start", partial: output });
} else if (item.contentBlockStart) {
if (!firstTokenTime) firstTokenTime = Date.now();
handleContentBlockStart(item.contentBlockStart, blocks, output, stream);
} else if (item.contentBlockDelta) {
if (!firstTokenTime) firstTokenTime = Date.now();
handleContentBlockDelta(item.contentBlockDelta, blocks, output, stream);
} else if (item.contentBlockStop) {
handleContentBlockStop(item.contentBlockStop, blocks, output, stream);
} else if (item.messageStop) {
output.stopReason = mapStopReason(item.messageStop.stopReason);
} else if (item.metadata) {
handleMetadata(item.metadata, model, output);
} else if (item.internalServerException) {
throw new Error(`Internal server error: ${item.internalServerException.message}`);
} else if (item.modelStreamErrorException) {
throw new Error(`Model stream error: ${item.modelStreamErrorException.message}`);
} else if (item.validationException) {
throw withHttpStatus(new Error(`Validation error: ${item.validationException.message}`), 400);
} else if (item.throttlingException) {
throw new Error(`Throttling error: ${item.throttlingException.message}`);
} else if (item.serviceUnavailableException) {
throw new Error(`Service unavailable: ${item.serviceUnavailableException.message}`);
}
}
stream.push({ type: "start", partial: output });
} else if (item.contentBlockStart) {
if (!firstTokenTime) firstTokenTime = Date.now();
handleContentBlockStart(item.contentBlockStart, blocks, output, stream);
} else if (item.contentBlockDelta) {
if (!firstTokenTime) firstTokenTime = Date.now();
handleContentBlockDelta(item.contentBlockDelta, blocks, output, stream);
} else if (item.contentBlockStop) {
handleContentBlockStop(item.contentBlockStop, blocks, output, stream);
} else if (item.messageStop) {
output.stopReason = mapStopReason(item.messageStop.stopReason);
} else if (item.metadata) {
handleMetadata(item.metadata, model, output);
} else if (item.internalServerException) {
throw new Error(`Internal server error: ${item.internalServerException.message}`);
} else if (item.modelStreamErrorException) {
throw new Error(`Model stream error: ${item.modelStreamErrorException.message}`);
} else if (item.validationException) {
throw withHttpStatus(new Error(`Validation error: ${item.validationException.message}`), 400);
} else if (item.throttlingException) {
throw new Error(`Throttling error: ${item.throttlingException.message}`);
} else if (item.serviceUnavailableException) {
throw new Error(`Service unavailable: ${item.serviceUnavailableException.message}`);
break;
} catch (error) {
if (
!usesHttp1RequestHandler &&
!messageStarted &&
output.content.length === 0 &&
isHttp2ResponseError(error)
) {
usesHttp1RequestHandler = true;
installBedrockHttp1Transport(config);
continue;
}
throw error;
} finally {
client.destroy();
}
}