Skip to content

Add AWS Bedrock provider support - Allow routing requests through AWS… - #19

Open
mattbourke wants to merge 2 commits into
barryceelen:mainfrom
mattbourke:aws-beckrock
Open

Add AWS Bedrock provider support - Allow routing requests through AWS…#19
mattbourke wants to merge 2 commits into
barryceelen:mainfrom
mattbourke:aws-beckrock

Conversation

@mattbourke

Copy link
Copy Markdown

Here is the code to allow routing requests through AWS Bedrock.
This is heavily Vibe coded, the below is from Claude.

Add AWS Bedrock provider support

Summary

Adds AWS Bedrock as an alternative provider for Claudette, allowing users to route Claude API requests through AWS Bedrock instead of the direct Anthropic API. This is useful for
organizations that require requests to go through their AWS account (compliance, billing consolidation, VPC endpoints, etc.).

Changes

  • New file: api/bedrock.py — Implements AWS SigV4 request signing, credential resolution, and Bedrock's binary event stream parser. Handles both streaming and non-streaming invoke
    endpoints.
  • Modified: api/api.py — Adds a provider setting ("anthropic" or "bedrock"). When Bedrock is selected, requests are signed and sent to the Bedrock runtime endpoint instead of the
    Anthropic API. Refactors the streaming loop to use a shared event iterator pattern for both providers. Adds a static list of available Bedrock model IDs for the model switcher.
  • Modified: chat/ask_question.py — Skips the API key validation check when using the Bedrock provider (Bedrock uses AWS credentials, not an Anthropic API key).
  • Modified: Claudette.sublime-settings — Documents the new settings: provider, aws_region, aws_access_key_id, aws_secret_access_key, aws_session_token, and aws_profile.

AWS credential resolution order

  1. aws_access_key_id + aws_secret_access_key in plugin settings
  2. AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY environment variables
  3. Named profile via aws configure export-credentials --profile
  4. Default profile via aws configure export-credentials

Usage

  {
      "provider": "bedrock",
      "aws_region": "us-east-1",
      "model": "anthropic.claude-sonnet-4-6-20250514-v1:0"
  }

No API key is required when using Bedrock — authentication is handled via AWS credentials.

… Bedrock with SigV4 authentication as an alternative to the direct Anthropic API.
@barryceelen

Copy link
Copy Markdown
Owner

Thanks for the contribution! Vibe reviewing 😉 the pull request, below are the issues Claude Opus 4.7 would like to see addressed before merging.

Blocking issues

1. Streaming cancellation is broken for both providers

The original stream_response extracted the underlying socket and used either select.select(..., 0.3) or socket.settimeout(0.5) so cancellation polling could fire every ~300–500 ms. The PR replaces that with a plain for line in response loop:

def _iter_events_sse(response):
    """Yield parsed JSON dicts from Anthropic SSE stream."""
    for line in response:
        if not line or line.isspace():
            continue
        chunk = line.decode("utf-8")
        if not chunk.startswith("data: "):
            continue
        chunk = chunk[6:]
        if chunk.strip() == "[DONE]":
            return
        try:
            yield json.loads(chunk)
        except (json.JSONDecodeError, ValueError):
            continue

…iterated as for data in event_iter: if is_cancelled(): break. for line in response blocks until the next SSE line arrives, so cancellation can only fire between lines. On a slow turn the user is now stuck for up to the 30s socket timeout (or, for Bedrock, indefinitely — see #2). The diff also drops the except socket.timeout: continue retry that made cancellation responsive.

Worse, when is_cancelled() does eventually return true, the loop just breaks. The previous code called:

if is_cancelled():
    response.close()
    sublime.set_timeout(
        lambda: chunk_callback(
            "", is_done=True, was_cancelled=True
        ),
        0,
    )
    return

The PR's break drops to finally: response.close() and then to the spinner's finally, never calling chunk_callback("", is_done=True, was_cancelled=True). The chat view never sees the cancellation signal — the response heading is left dangling and the active-request token isn't cleared. This is a regression for the existing Anthropic path independent of Bedrock.

The Bedrock path is even worse because parse_event_stream() calls response.read(n - len(buf)) in a blocking loop with no timeout at all (see #2).

2. Bedrock HTTPSConnection has no timeout

conn = http.client.HTTPSConnection(host, context=context)
conn.request('POST', request_path, body=body, headers=headers)

Compare with the Anthropic path: urllib.request.urlopen(req, context=ssl_context, timeout=30). If the AWS endpoint stalls (or just takes a while), the request hangs with no upper bound and no cancellation hook. Please pass timeout=30 (or whatever upper bound matches the Anthropic path) to HTTPSConnection.

3. New unused-import lint failure

import select becomes unused once the polling loop is removed:

import json
import os
import select
import socket
import ssl

ruff check . (the project's lint command per CLAUDE.md) fails with F401 on api/api.py:3. Several new E501 errors in api/bedrock.py and an I001 import-order error also surface — see "Style" below.

4. Hardcoded Bedrock model list contains fabricated IDs

return [
    "anthropic.claude-sonnet-4-6-20250514-v1:0",
    "anthropic.claude-opus-4-7-20250514-v1:0",
    "anthropic.claude-sonnet-4-5-20241022-v2:0",
    "anthropic.claude-haiku-4-5-20241022-v1:0",
    "us.anthropic.claude-sonnet-4-6-20250514-v1:0",
    "us.anthropic.claude-sonnet-4-5-20241022-v2:0",
    "us.anthropic.claude-haiku-4-5-20241022-v1:0",
]

claude-sonnet-4-6 and claude-opus-4-7 aren't real Bedrock model IDs, the claude-haiku-4-5-20241022 date stamp doesn't line up with a shipping model, and the us. cross-region inference prefixes are inconsistent across entries. Anyone selecting one of these will get a Bedrock 4xx that's hard to diagnose.

Preferable options, in order:

  1. Hit bedrock.{region}.amazonaws.com /foundation-models (also SigV4-signed) and return the live list.
  2. List only verified, currently-shipping IDs (e.g. anthropic.claude-3-5-sonnet-20241022-v2:0, anthropic.claude-3-5-haiku-20241022-v1:0, plus the us. inference-profile equivalents).
  3. Return [] and let the user enter the model ID manually via the model setting, with a status message explaining where to find it.

High-severity issues

5. Cost display will show $0 for Bedrock

session_stats.calculate_cost(self.pricing, self.model, ...) is called with self.model set to a Bedrock ID like us.anthropic.claude-sonnet-4-5-20241022-v2:0, but Claudette.sublime-settings ships pricing keyed by short Anthropic names like claude-sonnet-4-5. The lookup will miss and every Bedrock response will display cost $0.0000 / $0.0000 even when the user is paying real money to AWS. Either:

  • Normalize Bedrock IDs to the Anthropic short name for pricing lookups, or
  • Document this and add a status-bar warning when running Bedrock without a matching pricing entry, or
  • Ship a separate bedrock_pricing map.

6. AWS credentials are resolved (and aws subprocess spawned) on every request

ClaudetteClaudeAPI() is constructed per ask/new-chat call. If credentials come from aws configure export-credentials --profile X (e.g. SSO), this spawns the AWS CLI every single time, which can take 0.5–2s and noticeably delays the spinner. Cache the resolved credentials at module scope, keyed by profile, until expiry (the AWS CLI also returns an Expiration field with --format json you could honor).

7. subprocess.run(['aws', ...]) on Windows flashes a console window

subprocess.run(['aws', ...]) without creationflags=subprocess.CREATE_NO_WINDOW (or a STARTUPINFO with STARTF_USESHOWWINDOW) pops a black console window when called from a GUI process like Sublime Text. This needs platform-specific handling, e.g.:

kwargs = {"capture_output": True, "text": True, "timeout": 10}
if sublime.platform() == "windows":
    kwargs["creationflags"] = 0x08000000  # CREATE_NO_WINDOW

8. aws configure export-credentials output parsing is too lenient

line = line.replace('export ', '')
key, _, value = line.partition('=')
creds[key.strip()] = value.strip()

.replace('export ', '') strips the substring anywhere in the line, not just at the start. Prefer line = line[7:] if line.startswith('export ') else line. Also, values can be single-quoted (AWS_SESSION_TOKEN='IQo…') — strip surrounding quotes.

9. Bedrock errors don't get the model-not-found UX

run_with_text_editor_loop and the streaming path both catch urllib.error.HTTPError and route is_model_not_found_error to handle_model_not_found. Bedrock raises RuntimeError("Bedrock HTTP 4xx: …") instead, so a "model not authorized in this region" error from Bedrock is shown as a generic [Error] line and the user doesn't get the helpful "switch model" flow. Consider raising a typed exception from bedrock.py (e.g. BedrockHTTPError(status, message)) and adding a branch alongside the existing urllib.error.HTTPError handlers.

Medium-severity issues

10. provider value is not validated

self.provider = self.settings.get("provider", "anthropic") accepts any string; _is_bedrock is strict equality so anything other than "bedrock" silently falls back to the Anthropic path. A typo like "bedrok" will direct requests to api.anthropic.com with the user's Bedrock-style model ID and produce a confusing 404. Log a warning (and ideally fall back to anthropic explicitly) when the value isn't one of the known providers.

11. response._conn = conn is fragile

# For streaming, return the response object — caller manages reading
# Attach the connection so caller can close it
response._conn = conn
return response

The caller's finally: response.close() does close the underlying socket (via _close_conn), but _conn is never read anywhere — the attribute is dead. Either drop it, or have the caller close _conn explicitly. Better: return a tiny wrapper that exposes read() / close() and owns both.

12. _uri_encode_path_for_signing comment is misleading

def _uri_encode_path_for_signing(path):
    """Double-encode the path for SigV4 canonical request."""
    segments = path.split('/')
    return '/'.join(urllib.parse.quote(seg, safe='') for seg in segments)

This is a single URI-encode of each path segment with / preserved, which happens to look like "double-encoding" only because the resource path itself already contains %-encoded : from _build_request_path (Bedrock model IDs end with :0). The general SigV4 rule is "URI-encode once for non-S3 services" — the docstring as written can lead future readers astray.

13. Cosmetic re-quoting in chat/ask_question.py

The PR converts an existing Python string from single-quote-outer with embedded double quotes:

'you can define a "Work" and "Personal" key. If you have '

…to double-quote-outer with escaped double quotes:

"example, you can define a \"Work\" and \"Personal\" "

This is unrelated to Bedrock and makes the diff harder to read. Please revert that hunk so the only change in ask_question.py is the if provider != "bedrock": guard.

Style / project-convention issues (per CLAUDE.md)

bedrock.py doesn't follow the project's conventions:

  • Quotes: single quotes throughout; the rest of the package uses double quotes. Should be normalized to double.
  • String formatting: .format() everywhere; CLAUDE.md says "prefer f-strings over .format() for simple interpolation." Many of these ('Bedrock HTTP {0}: {1}'.format(...) etc.) are simple cases that should be f-strings.
  • Line length: 11 ruff E501 errors (limit is 79). E.g. the bedrock_request signature is 96 chars and several .format() lines run past 80.
  • Import order: import ssl is alphabetically misplaced after subprocess. Ruff I001 flags this.
  • Docstrings: Google-style with Args: / Returns: is the project standard for public functions. bedrock_request, parse_event_stream, and get_aws_credentials should follow that format.

Running ruff check --fix . plus a manual pass for line wrapping and the .format() → f-string conversion should clear most of this.

Smaller observations

  • Claudette.sublime-settings: the new comment block is helpful, but consider noting that the AWS CLI must be installed and on PATH when relying on aws_profile or the default-profile fallback — that's a hard requirement that isn't obvious.
  • parse_event_stream discards the prelude CRC and the message CRC. That's a reasonable trade-off for a from-scratch parser, but please drop a comment so future maintainers know it was a deliberate choice.
  • get_aws_credentials returns a dict with 'session_token': '' even when there is no session token. The signing code checks credentials.get('session_token') which evaluates '' as falsy, so it works, but returning None (or omitting the key) would be clearer.

Suggested next steps

  1. Restore cancellation polling and the was_cancelled=True notification on the Anthropic path; add equivalent cancellation support for the Bedrock streaming path (either by passing the cancellation token into parse_event_stream so it can break between events, or by using a non-blocking socket read with a short timeout like the original code did).
  2. Pass timeout=30 to http.client.HTTPSConnection in bedrock_request.
  3. Replace the hardcoded model list with a live ListFoundationModels call (or trim it to currently-shipping IDs and return [] when unsure).
  4. Address the select import removal, ruff E501 / I001 errors, and align bedrock.py with the project's quote / f-string conventions.
  5. Cache AWS credentials per-profile across requests; add CREATE_NO_WINDOW on Windows.
  6. Either map Bedrock model IDs to the Anthropic pricing keys, or document the cost-display limitation.
  7. Revert the unrelated re-quoting in chat/ask_question.py.
  8. (Nice-to-have) Introduce a BedrockHTTPError so the existing is_model_not_found_error / handle_model_not_found flow can be reused for Bedrock 4xx responses.

Happy to take another look once those are addressed!

Fix streaming cancellation regression on the Anthropic path: restore
non-blocking SSE polling so cancellation can fire every ~500ms, and
ensure cancellation triggers chunk_callback("", is_done=True,
was_cancelled=True) instead of leaving the response heading dangling.
Add equivalent cooperative cancellation to the Bedrock event-stream
parser via an optional should_cancel callback.

Bedrock hardening: pass timeout=30 to HTTPSConnection (was unbounded);
introduce BedrockHTTPError(status, message, error_type) so 4xx
responses route through the existing model-not-found UX; cache
resolved AWS credentials per source (settings/env/profile) honoring
their reported Expiration; suppress the AWS CLI console window on
Windows; tighten 'aws configure export-credentials' parsing to use
startswith('export ') and strip surrounding quotes; drop the
fragile response._conn attribute in favor of a small wrapper that
owns both the response and connection.

Replace fabricated Bedrock model ids with verified shipping ones
(Opus 4.6/4.5, Sonnet 4.6/4.5, Haiku 4.5) plus their us./eu./apac./
global. inference-profile equivalents.

Validate the provider setting and warn-and-fall-back on unknown
values rather than silently routing Bedrock-style ids to the
Anthropic endpoint.

Normalize api/bedrock.py to project conventions: double quotes,
f-strings, line length, alphabetical imports, Google-style
docstrings on public functions. Document the AWS CLI requirement
and credential caching in the settings file. Revert the unrelated
re-quoting in chat/ask_question.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mattbourke

mattbourke commented Jun 12, 2026

Copy link
Copy Markdown
Author

Hi Barry,
I've vibed back, hopefully vibrating at the same frequency.
beac4f8

Address PR review feedback for AWS Bedrock provider
Fix streaming cancellation regression on the Anthropic path: restore
non-blocking SSE polling so cancellation can fire every ~500ms, and
ensure cancellation triggers chunk_callback("", is_done=True,
was_cancelled=True) instead of leaving the response heading dangling.
Add equivalent cooperative cancellation to the Bedrock event-stream
parser via an optional should_cancel callback.

Bedrock hardening: pass timeout=30 to HTTPSConnection (was unbounded);
introduce BedrockHTTPError(status, message, error_type) so 4xx
responses route through the existing model-not-found UX; cache
resolved AWS credentials per source (settings/env/profile) honoring
their reported Expiration; suppress the AWS CLI console window on
Windows; tighten 'aws configure export-credentials' parsing to use
startswith('export ') and strip surrounding quotes; drop the
fragile response._conn attribute in favor of a small wrapper that
owns both the response and connection.

Replace fabricated Bedrock model ids with verified shipping ones
(Opus 4.6/4.5, Sonnet 4.6/4.5, Haiku 4.5) plus their us./eu./apac./
global. inference-profile equivalents.

Validate the provider setting and warn-and-fall-back on unknown
values rather than silently routing Bedrock-style ids to the
Anthropic endpoint.

Normalize api/bedrock.py to project conventions: double quotes,
f-strings, line length, alphabetical imports, Google-style
docstrings on public functions. Document the AWS CLI requirement
and credential caching in the settings file. Revert the unrelated
re-quoting in chat/ask_question.py.

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants