v0.4.4 e5506c1

discover_bedrock.go RawHistoryFolder

1package provider
2
3// Amazon Bedrock live model discovery.
4//
5// Enumerates the foundation models and cross-region inference profiles
6// available to the caller's AWS credentials, using the Bedrock control
7// plane (bedrock.{region}.amazonaws.com), SigV4-signed with the same
8// signer as the runtime client (packages/provider/amazon_bedrock.go):
9//
10//   - ListFoundationModels: the base foundation-model IDs.
11//   - ListInferenceProfiles: the us./eu./global./... prefixed IDs zot
12//     actually invokes for on-demand throughput.
13//
14// IMPORTANT auth note: the runtime bearer token (AWS_BEARER_TOKEN_BEDROCK,
15// scoped to bedrock:CallWithBearerToken) cannot call these control-plane
16// APIs. Discovery therefore requires SigV4 credentials (env keys, an
17// AWS_PROFILE, or CLI-resolved SSO/assume-role creds). When only a
18// bearer token is present, discovery is skipped and the built-in catalog
19// remains the source of truth.
20//
21// Pricing is intentionally NOT fetched. The AWS Price List API
22// (GetProducts, ServiceCode=AmazonBedrock) was evaluated against live
23// data and found to carry only stale, input-only rows for a handful of
24// legacy models (Claude 2.x / 3 Sonnet / 3 Haiku) with no output or
25// cache prices for any current model. See aws-cli issue #9567. The
26// hand-maintained catalog in catalog_builtin.go stays authoritative for
27// prices, context windows, and capability flags; MergeCatalog keeps that
28// metadata when a discovered ID matches a catalog entry, and unknown
29// discovered IDs surface in the picker with placeholder prices.
30
31import (
32	"bytes"
33	"context"
34	"encoding/json"
35	"fmt"
36	"io"
37	"net/http"
38	"net/url"
39	"strings"
40	"time"
41)
42
43// bytesReader returns a reader for payload, or nil when payload is nil
44// so GET requests carry no body.
45func bytesReader(payload []byte) io.Reader {
46	if payload == nil {
47		return nil
48	}
49	return bytes.NewReader(payload)
50}
51
52// DiscoverBedrock lists the foundation models and inference profiles
53// available to the caller's AWS credentials in the given region.
54//
55// SigV4 credentials are resolved from the standard AWS environment /
56// profile / CLI sources, even when a bearer token is used for inference.
57//
58// region defaults to AWS_REGION / AWS_DEFAULT_REGION / us-east-1 when
59// empty. A nil error with a nil slice means "no SigV4 credentials" or
60// "no models"; callers should treat that as a skip.
61func DiscoverBedrock(ctx context.Context, region string) ([]Model, error) {
62	if region == "" {
63		region = bedrockResolveRegion()
64	}
65	sigv4 := resolveBedrockDiscoveryCreds()
66	if sigv4 == nil {
67		// Bearer-only or unauthenticated: control-plane APIs are
68		// unreachable. Skip quietly so the catalog stands.
69		return nil, nil
70	}
71
72	client := &http.Client{Timeout: 15 * time.Second}
73	ids, err := bedrockListModelIDs(ctx, client, sigv4, region, "https://bedrock."+region+".amazonaws.com")
74	if err != nil {
75		return nil, err
76	}
77	if len(ids) == 0 {
78		return nil, nil
79	}
80
81	out := make([]Model, 0, len(ids))
82	for _, id := range ids {
83		out = append(out, Model{
84			Provider: "amazon-bedrock",
85			ID:       id,
86			Source:   "live",
87			BaseURL:  "https://bedrock-runtime." + region + ".amazonaws.com",
88		})
89	}
90	return out, nil
91}
92
93// bedrockListModelIDs enumerates foundation-model IDs and inference-
94// profile IDs available in the region. Profile IDs are the geo-prefixed
95// forms zot invokes; both are returned so the picker shows every
96// selectable variant. controlPlaneBase is the scheme+host (no trailing
97// slash) of the Bedrock control plane, parameterized for testing.
98func bedrockListModelIDs(ctx context.Context, client *http.Client, creds *bedrockSigV4Creds, region, controlPlaneBase string) ([]string, error) {
99	seen := map[string]bool{}
100	var ids []string
101	add := func(id string) {
102		id = strings.TrimSpace(id)
103		if id == "" || seen[id] {
104			return
105		}
106		seen[id] = true
107		ids = append(ids, id)
108	}
109
110	// ListFoundationModels: GET, no pagination token in the response.
111	fmURL := controlPlaneBase + "/foundation-models"
112	body, err := bedrockSignedControlPlane(ctx, client, creds, region, http.MethodGet, fmURL, nil)
113	if err != nil {
114		return nil, err
115	}
116	var fm struct {
117		ModelSummaries []struct {
118			ModelID          string   `json:"modelId"`
119			OutputModalities []string `json:"outputModalities"`
120		} `json:"modelSummaries"`
121	}
122	if err := json.Unmarshal(body, &fm); err != nil {
123		return nil, fmt.Errorf("bedrock discover: parse foundation-models: %w", err)
124	}
125	for _, s := range fm.ModelSummaries {
126		// Keep text-output models only; skip embeddings/image generators.
127		if !bedrockEmitsText(s.OutputModalities) {
128			continue
129		}
130		add(s.ModelID)
131	}
132
133	// ListInferenceProfiles: GET, paginated via ?nextToken=.
134	next := ""
135	for {
136		query := url.Values{"maxResults": {"1000"}}
137		if next != "" {
138			query.Set("nextToken", next)
139		}
140		ipURL := controlPlaneBase + "/inference-profiles?" + query.Encode()
141		ipBody, err := bedrockSignedControlPlane(ctx, client, creds, region, http.MethodGet, ipURL, nil)
142		if err != nil {
143			// Non-fatal: foundation models already gathered.
144			break
145		}
146		var ip struct {
147			InferenceProfileSummaries []struct {
148				InferenceProfileID string `json:"inferenceProfileId"`
149			} `json:"inferenceProfileSummaries"`
150			NextToken string `json:"nextToken"`
151		}
152		if err := json.Unmarshal(ipBody, &ip); err != nil {
153			break
154		}
155		for _, s := range ip.InferenceProfileSummaries {
156			add(s.InferenceProfileID)
157		}
158		if ip.NextToken == "" {
159			break
160		}
161		next = ip.NextToken
162	}
163
164	return ids, nil
165}
166
167func bedrockEmitsText(modalities []string) bool {
168	if len(modalities) == 0 {
169		return true // Unknown: assume usable rather than hide it.
170	}
171	for _, m := range modalities {
172		if strings.EqualFold(m, "TEXT") {
173			return true
174		}
175	}
176	return false
177}
178
179// bedrockSignedControlPlane issues a SigV4-signed request to a Bedrock
180// control-plane endpoint and returns the response body. Non-2xx is an
181// error carrying the status and trimmed body.
182func bedrockSignedControlPlane(ctx context.Context, client *http.Client, creds *bedrockSigV4Creds, region, method, url string, payload []byte) ([]byte, error) {
183	req, err := http.NewRequestWithContext(ctx, method, url, bytesReader(payload))
184	if err != nil {
185		return nil, err
186	}
187	if payload != nil {
188		req.Header.Set("content-type", "application/json")
189	}
190	if err := signSigV4(req, payload, "bedrock", region, creds, time.Now().UTC()); err != nil {
191		return nil, fmt.Errorf("bedrock discover: sign: %w", err)
192	}
193	resp, err := client.Do(req)
194	if err != nil {
195		return nil, fmt.Errorf("bedrock discover: %w", err)
196	}
197	defer resp.Body.Close()
198	b, _ := io.ReadAll(io.LimitReader(resp.Body, 16<<20))
199	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
200		return nil, fmt.Errorf("bedrock discover: http %d: %s", resp.StatusCode, strings.TrimSpace(string(b)))
201	}
202	return b, nil
203}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close