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:
@@ -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=="],
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user