From ca875ad5f9926da592c41dd1757a2f91c1eb88c8 Mon Sep 17 00:00:00 2001 From: can1357 Date: Fri, 24 Apr 2026 00:55:32 +0200 Subject: [PATCH] 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. --- bun.lock | 10 +- docs/environment-variables.md | 2 + package.json | 3 + packages/ai/CHANGELOG.md | 4 + packages/ai/package.json | 2 + packages/ai/src/providers/amazon-bedrock.ts | 135 +++++++++++++++----- 6 files changed, 120 insertions(+), 36 deletions(-) diff --git a/bun.lock b/bun.lock index 423d4fbae..cda073a74 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/docs/environment-variables.md b/docs/environment-variables.md index 222a44ac5..a5299c0ed 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -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`. diff --git a/package.json b/package.json index 3abc678ca..673ab46f3 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 87d5b2e1f..5405c8e67 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -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. diff --git a/packages/ai/package.json b/packages/ai/package.json index 12f2ad00d..46fbb08d0 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -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": { diff --git a/packages/ai/src/providers/amazon-bedrock.ts b/packages/ai/src/providers/amazon-bedrock.ts index 1a125afac..42bbcc671 100644 --- a/packages/ai/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -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 { + 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(); } }