Merge pull request #210 from rgasper/opus55-bedrock
Update bedrock models - add new opus 5.5, correct pricing details, add live pull
9 files · +799 -23
M README.md +25 -1
| @@ -544,7 +544,7 @@ Use `/login` to store API keys or subscription credentials. `/model` only shows | |||
| 544 | 544 | `--list-models` or the `/model` picker shows the full catalog across all built-in providers. Three sources: | |
| 545 | 545 | ||
| 546 | 546 | - **Catalog**: models baked into zot, covering Claude, GPT/Codex, Gemini/Gemma, Kimi/Moonshot, DeepSeek, Groq-hosted Llama/Gemma/Compound, OpenRouter-routed models, Bedrock model ids, Vertex model ids, Azure OpenAI deployments, Copilot models, and other provider-specific catalog entries. | |
| 547 | - | - **Live**: IDs discovered from `GET /v1/models` using your stored API key (cached for 6h in `$ZOT_HOME/models-cache.json`, refreshed in the background on startup). | |
| 547 | + | - **Live**: IDs discovered from the provider using your stored credentials — usually `GET /v1/models`, or provider-specific listing APIs for Amazon Bedrock (see [Amazon Bedrock](#amazon-bedrock)). Cached for 6h in `$ZOT_HOME/models-cache.json` and refreshed in the background on startup. | |
| 548 | 548 | - **Speculative**: IDs that appear in the upstream generator but aren't live on the public API yet. They'll 404 today and start working the moment the provider ships them. | |
| 549 | 549 | ||
| 550 | 550 | The context meter in the status line uses the model's advertised context window to show how much of it your last turn consumed. | |
| @@ -785,6 +785,30 @@ Access tokens are cached in memory and refreshed on demand. | |||
| 785 | 785 | ||
| 786 | 786 | If none of these are available, zot errors with `vertex: no auth — set GOOGLE_CLOUD_API_KEY or GOOGLE_APPLICATION_CREDENTIALS`. | |
| 787 | 787 | ||
| 788 | + | ### Amazon Bedrock | |
| 789 | + | ||
| 790 | + | zot talks to Bedrock's Converse streaming API and supports two credential styles: | |
| 791 | + | ||
| 792 | + | - **Bedrock API key** (bearer token) via `AWS_BEARER_TOKEN_BEDROCK`. This is the simplest setup: a single opaque string, region-scoped to wherever it was minted. It requires only the `bedrock:CallWithBearerToken` permission. | |
| 793 | + | - **SigV4** via `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` (plus optional `AWS_SESSION_TOKEN`), an `AWS_PROFILE`, or a default / SSO / assume-role / `credential_process` profile resolved through the AWS CLI. SigV4 (AWS Signature Version 4) is AWS's request-signing scheme: your secret key signs each request locally and only the signature travels over the wire. zot implements it directly so it does not depend on the AWS SDK. | |
| 794 | + | ||
| 795 | + | If both are present, the bearer token wins for inference, while live model discovery uses SigV4. The region defaults to `AWS_REGION` / `AWS_DEFAULT_REGION`, falling back to `us-east-1`. A `403` on a freshly-copied API key almost always means a region mismatch — set `AWS_REGION` to the region the key was created in, or pass `--base-url https://bedrock-runtime.<region>.amazonaws.com`. | |
| 796 | + | ||
| 797 | + | #### Catalog vs. live discovery | |
| 798 | + | ||
| 799 | + | Unlike most providers, Bedrock model **pricing, context windows, and capability flags are hand-maintained** in zot's baked-in catalog. AWS does not expose usable pricing programmatically for current models — its Price List API returns only stale, input-only rates for a few legacy models — so the catalog is authoritative for cost and metadata. This means: | |
| 800 | + | ||
| 801 | + | - **Bearer-token-only users** still get the full curated Bedrock model list with correct pricing from the catalog. No AWS API listing is attempted. | |
| 802 | + | - **Users with SigV4 credentials** additionally get **live model discovery**, even if inference uses a bearer token: on startup, zot lists the foundation models and cross-region inference profiles your account can actually use (via `ListFoundationModels` and `ListInferenceProfiles`) and merges them into the catalog. New Bedrock models therefore appear in `/model` without waiting for a zot release. Discovered IDs already known to the catalog keep their curated pricing; genuinely new IDs show up with placeholder pricing until the catalog is updated. | |
| 803 | + | ||
| 804 | + | Live discovery requires SigV4 because the listing APIs live on the Bedrock control plane, which rejects the runtime bearer token. It runs in the background at startup and is cached for 6h like other providers, so `/model` reflects the latest list without any manual refresh. If discovery fails (no SigV4 creds, expired SSO login, missing IAM permissions), zot silently falls back to the catalog. Discovery needs `bedrock:ListFoundationModels` and `bedrock:ListInferenceProfiles`. | |
| 805 | + | ||
| 806 | + | #### Cross-region pricing caveat | |
| 807 | + | ||
| 808 | + | Bedrock model IDs carry a routing prefix: `global.` (global cross-region inference), a geography such as `us.` / `eu.` / `apac.` / `au.` / `jp.` (geo cross-region inference), or none (in-region). zot's catalog prices **every** variant at the model's base (US) rate. | |
| 809 | + | ||
| 810 | + | AWS charges a surcharge for **geo cross-region inference** in some regions — for example, Anthropic models routed through the Asia Pacific (Melbourne) geo profile cost about 10% more than the base rate. zot does **not** yet capture these per-region uplifts, so if you use a geo-prefixed profile in a region that charges the surcharge, zot's reported cost may be **lower than your actual AWS bill**. Global cross-region and plain in-region usage match the base rate and are unaffected. The context/usage accounting is still correct; only the dollar figure can under-report in this edge case. If you need exact costs, check the per-region table on the [Amazon Bedrock pricing page](https://aws.amazon.com/bedrock/pricing/). | |
| 811 | + | ||
| 788 | 812 | ### Local models with ollama | |
| 789 | 813 | ||
| 790 | 814 | zot works with [ollama](https://ollama.com) out of the box. Ollama serves an OpenAI-compatible API locally, so any model you have pulled works with zot. | |
| @@ -544,7 +544,7 @@ Use `/login` to store API keys or subscription credentials. `/model` only shows | |||||
| 544 | `--list-models` or the `/model` picker shows the full catalog across all built-in providers. Three sources: | 544 | `--list-models` or the `/model` picker shows the full catalog across all built-in providers. Three sources: | ||
| 545 | 545 | ||||
| 546 | - **Catalog**: models baked into zot, covering Claude, GPT/Codex, Gemini/Gemma, Kimi/Moonshot, DeepSeek, Groq-hosted Llama/Gemma/Compound, OpenRouter-routed models, Bedrock model ids, Vertex model ids, Azure OpenAI deployments, Copilot models, and other provider-specific catalog entries. | 546 | - **Catalog**: models baked into zot, covering Claude, GPT/Codex, Gemini/Gemma, Kimi/Moonshot, DeepSeek, Groq-hosted Llama/Gemma/Compound, OpenRouter-routed models, Bedrock model ids, Vertex model ids, Azure OpenAI deployments, Copilot models, and other provider-specific catalog entries. | ||
| 547 | - | - **Live**: IDs discovered from `GET /v1/models` using your stored API key (cached for 6h in `$ZOT_HOME/models-cache.json`, refreshed in the background on startup). | 547 | + | - **Live**: IDs discovered from the provider using your stored credentials — usually `GET /v1/models`, or provider-specific listing APIs for Amazon Bedrock (see [Amazon Bedrock](#amazon-bedrock)). Cached for 6h in `$ZOT_HOME/models-cache.json` and refreshed in the background on startup. |
| 548 | - **Speculative**: IDs that appear in the upstream generator but aren't live on the public API yet. They'll 404 today and start working the moment the provider ships them. | 548 | - **Speculative**: IDs that appear in the upstream generator but aren't live on the public API yet. They'll 404 today and start working the moment the provider ships them. | ||
| 549 | 549 | ||||
| 550 | The context meter in the status line uses the model's advertised context window to show how much of it your last turn consumed. | 550 | The context meter in the status line uses the model's advertised context window to show how much of it your last turn consumed. | ||
| @@ -785,6 +785,30 @@ Access tokens are cached in memory and refreshed on demand. | |||||
| 785 | 785 | ||||
| 786 | If none of these are available, zot errors with `vertex: no auth — set GOOGLE_CLOUD_API_KEY or GOOGLE_APPLICATION_CREDENTIALS`. | 786 | If none of these are available, zot errors with `vertex: no auth — set GOOGLE_CLOUD_API_KEY or GOOGLE_APPLICATION_CREDENTIALS`. | ||
| 787 | 787 | ||||
| 788 | + | ### Amazon Bedrock | |||
| 789 | + | ||||
| 790 | + | zot talks to Bedrock's Converse streaming API and supports two credential styles: | |||
| 791 | + | ||||
| 792 | + | - **Bedrock API key** (bearer token) via `AWS_BEARER_TOKEN_BEDROCK`. This is the simplest setup: a single opaque string, region-scoped to wherever it was minted. It requires only the `bedrock:CallWithBearerToken` permission. | |||
| 793 | + | - **SigV4** via `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` (plus optional `AWS_SESSION_TOKEN`), an `AWS_PROFILE`, or a default / SSO / assume-role / `credential_process` profile resolved through the AWS CLI. SigV4 (AWS Signature Version 4) is AWS's request-signing scheme: your secret key signs each request locally and only the signature travels over the wire. zot implements it directly so it does not depend on the AWS SDK. | |||
| 794 | + | ||||
| 795 | + | If both are present, the bearer token wins for inference, while live model discovery uses SigV4. The region defaults to `AWS_REGION` / `AWS_DEFAULT_REGION`, falling back to `us-east-1`. A `403` on a freshly-copied API key almost always means a region mismatch — set `AWS_REGION` to the region the key was created in, or pass `--base-url https://bedrock-runtime.<region>.amazonaws.com`. | |||
| 796 | + | ||||
| 797 | + | #### Catalog vs. live discovery | |||
| 798 | + | ||||
| 799 | + | Unlike most providers, Bedrock model **pricing, context windows, and capability flags are hand-maintained** in zot's baked-in catalog. AWS does not expose usable pricing programmatically for current models — its Price List API returns only stale, input-only rates for a few legacy models — so the catalog is authoritative for cost and metadata. This means: | |||
| 800 | + | ||||
| 801 | + | - **Bearer-token-only users** still get the full curated Bedrock model list with correct pricing from the catalog. No AWS API listing is attempted. | |||
| 802 | + | - **Users with SigV4 credentials** additionally get **live model discovery**, even if inference uses a bearer token: on startup, zot lists the foundation models and cross-region inference profiles your account can actually use (via `ListFoundationModels` and `ListInferenceProfiles`) and merges them into the catalog. New Bedrock models therefore appear in `/model` without waiting for a zot release. Discovered IDs already known to the catalog keep their curated pricing; genuinely new IDs show up with placeholder pricing until the catalog is updated. | |||
| 803 | + | ||||
| 804 | + | Live discovery requires SigV4 because the listing APIs live on the Bedrock control plane, which rejects the runtime bearer token. It runs in the background at startup and is cached for 6h like other providers, so `/model` reflects the latest list without any manual refresh. If discovery fails (no SigV4 creds, expired SSO login, missing IAM permissions), zot silently falls back to the catalog. Discovery needs `bedrock:ListFoundationModels` and `bedrock:ListInferenceProfiles`. | |||
| 805 | + | ||||
| 806 | + | #### Cross-region pricing caveat | |||
| 807 | + | ||||
| 808 | + | Bedrock model IDs carry a routing prefix: `global.` (global cross-region inference), a geography such as `us.` / `eu.` / `apac.` / `au.` / `jp.` (geo cross-region inference), or none (in-region). zot's catalog prices **every** variant at the model's base (US) rate. | |||
| 809 | + | ||||
| 810 | + | AWS charges a surcharge for **geo cross-region inference** in some regions — for example, Anthropic models routed through the Asia Pacific (Melbourne) geo profile cost about 10% more than the base rate. zot does **not** yet capture these per-region uplifts, so if you use a geo-prefixed profile in a region that charges the surcharge, zot's reported cost may be **lower than your actual AWS bill**. Global cross-region and plain in-region usage match the base rate and are unaffected. The context/usage accounting is still correct; only the dollar figure can under-report in this edge case. If you need exact costs, check the per-region table on the [Amazon Bedrock pricing page](https://aws.amazon.com/bedrock/pricing/). | |||
| 811 | + | ||||
| 788 | ### Local models with ollama | 812 | ### Local models with ollama | ||
| 789 | 813 | ||||
| 790 | zot works with [ollama](https://ollama.com) out of the box. Ollama serves an OpenAI-compatible API locally, so any model you have pulled works with zot. | 814 | zot works with [ollama](https://ollama.com) out of the box. Ollama serves an OpenAI-compatible API locally, so any model you have pulled works with zot. | ||
M packages/agent/build.go +6 -0
| @@ -596,6 +596,12 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||
| 596 | 596 | // models like ollama don't need real API keys). | |
| 597 | 597 | if resolvedModel.BaseURL != "" && credErr != nil { | |
| 598 | 598 | cred = "ollama" | |
| 599 | + | if provName == "amazon-bedrock" { | |
| 600 | + | // The default AWS CLI profile is resolved by the Bedrock client, | |
| 601 | + | // not the general credential resolver. Never pass the local-model | |
| 602 | + | // placeholder as a bearer token. | |
| 603 | + | cred = "<aws>" | |
| 604 | + | } | |
| 599 | 605 | credErr = nil | |
| 600 | 606 | requireCred = false | |
| 601 | 607 | } | |
| @@ -596,6 +596,12 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||||
| 596 | // models like ollama don't need real API keys). | 596 | // models like ollama don't need real API keys). | ||
| 597 | if resolvedModel.BaseURL != "" && credErr != nil { | 597 | if resolvedModel.BaseURL != "" && credErr != nil { | ||
| 598 | cred = "ollama" | 598 | cred = "ollama" | ||
| 599 | + | if provName == "amazon-bedrock" { | |||
| 600 | + | // The default AWS CLI profile is resolved by the Bedrock client, | |||
| 601 | + | // not the general credential resolver. Never pass the local-model | |||
| 602 | + | // placeholder as a bearer token. | |||
| 603 | + | cred = "<aws>" | |||
| 604 | + | } | |||
| 599 | credErr = nil | 605 | credErr = nil | ||
| 600 | requireCred = false | 606 | requireCred = false | ||
| 601 | } | 607 | } | ||
M packages/agent/modelsync.go +6 -0
| @@ -255,6 +255,12 @@ func refreshModels() { | |||
| 255 | 255 | all = append(all, live...) | |
| 256 | 256 | } | |
| 257 | 257 | } | |
| 258 | + | // Bedrock resolves SigV4 credentials independently of inference auth. | |
| 259 | + | // A default AWS CLI profile can be usable even when zot has no | |
| 260 | + | // Bedrock credential registered with its general resolver. | |
| 261 | + | if live, err := provider.DiscoverBedrock(ctx, ""); err == nil { | |
| 262 | + | all = append(all, live...) | |
| 263 | + | } | |
| 258 | 264 | if haveOpenRouter { | |
| 259 | 265 | // /models is public; gate on a credential so the picker only | |
| 260 | 266 | // fills with OpenRouter's hundreds of routes for users who use it. | |
| @@ -255,6 +255,12 @@ func refreshModels() { | |||||
| 255 | all = append(all, live...) | 255 | all = append(all, live...) | ||
| 256 | } | 256 | } | ||
| 257 | } | 257 | } | ||
| 258 | + | // Bedrock resolves SigV4 credentials independently of inference auth. | |||
| 259 | + | // A default AWS CLI profile can be usable even when zot has no | |||
| 260 | + | // Bedrock credential registered with its general resolver. | |||
| 261 | + | if live, err := provider.DiscoverBedrock(ctx, ""); err == nil { | |||
| 262 | + | all = append(all, live...) | |||
| 263 | + | } | |||
| 258 | if haveOpenRouter { | 264 | if haveOpenRouter { | ||
| 259 | // /models is public; gate on a credential so the picker only | 265 | // /models is public; gate on a credential so the picker only | ||
| 260 | // fills with OpenRouter's hundreds of routes for users who use it. | 266 | // fills with OpenRouter's hundreds of routes for users who use it. | ||
M packages/agent/modelsync_test.go +52 -0
| @@ -4,16 +4,68 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "encoding/json" | |
| 6 | 6 | "fmt" | |
| 7 | + | "io" | |
| 7 | 8 | "net/http" | |
| 8 | 9 | "net/http/httptest" | |
| 9 | 10 | "os" | |
| 10 | 11 | "path/filepath" | |
| 12 | + | "runtime" | |
| 13 | + | "strings" | |
| 11 | 14 | "testing" | |
| 12 | 15 | "time" | |
| 13 | 16 | ||
| 14 | 17 | "github.com/patriceckhart/zot/packages/provider" | |
| 15 | 18 | ) | |
| 16 | 19 | ||
| 20 | + | func TestRefreshModelsBedrockWithDefaultCLIProfile(t *testing.T) { | |
| 21 | + | if runtime.GOOS == "windows" { | |
| 22 | + | t.Skip("test uses a shell script to simulate the AWS CLI") | |
| 23 | + | } | |
| 24 | + | t.Setenv("ZOT_HOME", t.TempDir()) | |
| 25 | + | t.Setenv("AWS_PROFILE", "") | |
| 26 | + | t.Setenv("AWS_ACCESS_KEY_ID", "") | |
| 27 | + | t.Setenv("AWS_SECRET_ACCESS_KEY", "") | |
| 28 | + | t.Setenv("AWS_BEARER_TOKEN_BEDROCK", "") | |
| 29 | + | for _, name := range []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "KIMI_API_KEY", "GOOGLE_API_KEY", "OPENROUTER_API_KEY", "GONDOLA_API_KEY", "YOLO_AUTO_API_KEY"} { | |
| 30 | + | t.Setenv(name, "") | |
| 31 | + | } | |
| 32 | + | bin := t.TempDir() | |
| 33 | + | script := "#!/bin/sh\nprintf '%s\\n' '{\"AccessKeyId\":\"AKID\",\"SecretAccessKey\":\"SECRET\"}'\n" | |
| 34 | + | if err := os.WriteFile(filepath.Join(bin, "aws"), []byte(script), 0o755); err != nil { | |
| 35 | + | t.Fatal(err) | |
| 36 | + | } | |
| 37 | + | t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH")) | |
| 38 | + | orig := http.DefaultTransport | |
| 39 | + | t.Cleanup(func() { | |
| 40 | + | http.DefaultTransport = orig | |
| 41 | + | provider.SetLiveModels(nil) | |
| 42 | + | }) | |
| 43 | + | http.DefaultTransport = yoloAutoTestTransport(func(r *http.Request) (*http.Response, error) { | |
| 44 | + | var body string | |
| 45 | + | switch r.URL.Path { | |
| 46 | + | case "/foundation-models": | |
| 47 | + | body = `{"modelSummaries":[{"modelId":"example.cli-only-model","outputModalities":["TEXT"]}]}` | |
| 48 | + | case "/inference-profiles": | |
| 49 | + | body = `{"inferenceProfileSummaries":[]}` | |
| 50 | + | default: | |
| 51 | + | return nil, fmt.Errorf("unexpected discovery request: %s", r.URL) | |
| 52 | + | } | |
| 53 | + | return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)}, nil | |
| 54 | + | }) | |
| 55 | + | ||
| 56 | + | refreshModels() | |
| 57 | + | if _, err := provider.FindModel("amazon-bedrock", "example.cli-only-model"); err != nil { | |
| 58 | + | t.Fatal("default AWS CLI profile was not discovered:", err) | |
| 59 | + | } | |
| 60 | + | resolved, err := Resolve(Args{Provider: "amazon-bedrock", Model: "example.cli-only-model"}, true) | |
| 61 | + | if err != nil { | |
| 62 | + | t.Fatal("default AWS CLI profile could not resolve for inference:", err) | |
| 63 | + | } | |
| 64 | + | if resolved.Credential != "<aws>" { | |
| 65 | + | t.Fatalf("inference credential = %q, want AWS sentinel", resolved.Credential) | |
| 66 | + | } | |
| 67 | + | } | |
| 68 | + | ||
| 17 | 69 | func TestRefreshLlamaCPPModelsAddsOnlyLoadedModels(t *testing.T) { | |
| 18 | 70 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 19 | 71 | if r.URL.Path != "/models" { | |
| @@ -4,16 +4,68 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "encoding/json" | 5 | "encoding/json" | ||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | + | "io" | |||
| 7 | "net/http" | 8 | "net/http" | ||
| 8 | "net/http/httptest" | 9 | "net/http/httptest" | ||
| 9 | "os" | 10 | "os" | ||
| 10 | "path/filepath" | 11 | "path/filepath" | ||
| 12 | + | "runtime" | |||
| 13 | + | "strings" | |||
| 11 | "testing" | 14 | "testing" | ||
| 12 | "time" | 15 | "time" | ||
| 13 | 16 | ||||
| 14 | "github.com/patriceckhart/zot/packages/provider" | 17 | "github.com/patriceckhart/zot/packages/provider" | ||
| 15 | ) | 18 | ) | ||
| 16 | 19 | ||||
| 20 | + | func TestRefreshModelsBedrockWithDefaultCLIProfile(t *testing.T) { | |||
| 21 | + | if runtime.GOOS == "windows" { | |||
| 22 | + | t.Skip("test uses a shell script to simulate the AWS CLI") | |||
| 23 | + | } | |||
| 24 | + | t.Setenv("ZOT_HOME", t.TempDir()) | |||
| 25 | + | t.Setenv("AWS_PROFILE", "") | |||
| 26 | + | t.Setenv("AWS_ACCESS_KEY_ID", "") | |||
| 27 | + | t.Setenv("AWS_SECRET_ACCESS_KEY", "") | |||
| 28 | + | t.Setenv("AWS_BEARER_TOKEN_BEDROCK", "") | |||
| 29 | + | for _, name := range []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "KIMI_API_KEY", "GOOGLE_API_KEY", "OPENROUTER_API_KEY", "GONDOLA_API_KEY", "YOLO_AUTO_API_KEY"} { | |||
| 30 | + | t.Setenv(name, "") | |||
| 31 | + | } | |||
| 32 | + | bin := t.TempDir() | |||
| 33 | + | script := "#!/bin/sh\nprintf '%s\\n' '{\"AccessKeyId\":\"AKID\",\"SecretAccessKey\":\"SECRET\"}'\n" | |||
| 34 | + | if err := os.WriteFile(filepath.Join(bin, "aws"), []byte(script), 0o755); err != nil { | |||
| 35 | + | t.Fatal(err) | |||
| 36 | + | } | |||
| 37 | + | t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH")) | |||
| 38 | + | orig := http.DefaultTransport | |||
| 39 | + | t.Cleanup(func() { | |||
| 40 | + | http.DefaultTransport = orig | |||
| 41 | + | provider.SetLiveModels(nil) | |||
| 42 | + | }) | |||
| 43 | + | http.DefaultTransport = yoloAutoTestTransport(func(r *http.Request) (*http.Response, error) { | |||
| 44 | + | var body string | |||
| 45 | + | switch r.URL.Path { | |||
| 46 | + | case "/foundation-models": | |||
| 47 | + | body = `{"modelSummaries":[{"modelId":"example.cli-only-model","outputModalities":["TEXT"]}]}` | |||
| 48 | + | case "/inference-profiles": | |||
| 49 | + | body = `{"inferenceProfileSummaries":[]}` | |||
| 50 | + | default: | |||
| 51 | + | return nil, fmt.Errorf("unexpected discovery request: %s", r.URL) | |||
| 52 | + | } | |||
| 53 | + | return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)}, nil | |||
| 54 | + | }) | |||
| 55 | + | ||||
| 56 | + | refreshModels() | |||
| 57 | + | if _, err := provider.FindModel("amazon-bedrock", "example.cli-only-model"); err != nil { | |||
| 58 | + | t.Fatal("default AWS CLI profile was not discovered:", err) | |||
| 59 | + | } | |||
| 60 | + | resolved, err := Resolve(Args{Provider: "amazon-bedrock", Model: "example.cli-only-model"}, true) | |||
| 61 | + | if err != nil { | |||
| 62 | + | t.Fatal("default AWS CLI profile could not resolve for inference:", err) | |||
| 63 | + | } | |||
| 64 | + | if resolved.Credential != "<aws>" { | |||
| 65 | + | t.Fatalf("inference credential = %q, want AWS sentinel", resolved.Credential) | |||
| 66 | + | } | |||
| 67 | + | } | |||
| 68 | + | ||||
| 17 | func TestRefreshLlamaCPPModelsAddsOnlyLoadedModels(t *testing.T) { | 69 | func TestRefreshLlamaCPPModelsAddsOnlyLoadedModels(t *testing.T) { | ||
| 18 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 70 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| 19 | if r.URL.Path != "/models" { | 71 | if r.URL.Path != "/models" { | ||
M packages/provider/amazon_bedrock.go +97 -19
| @@ -50,6 +50,7 @@ import ( | |||
| 50 | 50 | "net/http" | |
| 51 | 51 | "net/url" | |
| 52 | 52 | "os" | |
| 53 | + | "os/exec" | |
| 53 | 54 | "sort" | |
| 54 | 55 | "strings" | |
| 55 | 56 | "time" | |
| @@ -82,17 +83,13 @@ type bedrockSigV4Creds struct { | |||
| 82 | 83 | // 3. AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY (+ optional | |
| 83 | 84 | // AWS_SESSION_TOKEN) -> SigV4 route. | |
| 84 | 85 | // 4. AWS_PROFILE -> read ~/.aws/credentials, take that profile's keys. | |
| 86 | + | // 5. AWS_PROFILE -> `aws configure export-credentials` (SSO, assume-role, | |
| 87 | + | // credential_process) when the profile has no static keys. | |
| 85 | 88 | // | |
| 86 | 89 | // region defaults to us-east-1 unless AWS_REGION / AWS_DEFAULT_REGION is | |
| 87 | 90 | // set or the baseURL embeds a region. | |
| 88 | 91 | func NewBedrockClient(apiKey, baseURL string) Client { | |
| 89 | - | region := os.Getenv("AWS_REGION") | |
| 90 | - | if region == "" { | |
| 91 | - | region = os.Getenv("AWS_DEFAULT_REGION") | |
| 92 | - | } | |
| 93 | - | if region == "" { | |
| 94 | - | region = "us-east-1" | |
| 95 | - | } | |
| 92 | + | region := bedrockResolveRegion() | |
| 96 | 93 | if baseURL == "" { | |
| 97 | 94 | baseURL = "https://bedrock-runtime." + region + ".amazonaws.com" | |
| 98 | 95 | } | |
| @@ -102,36 +99,117 @@ func NewBedrockClient(apiKey, baseURL string) Client { | |||
| 102 | 99 | http: &http.Client{Timeout: 0}, | |
| 103 | 100 | } | |
| 104 | 101 | ||
| 105 | - | // Bearer route. | |
| 102 | + | bearer, sigv4 := resolveBedrockAuth(apiKey) | |
| 103 | + | if bearer != "" { | |
| 104 | + | c.bearerToken = bearer | |
| 105 | + | return c | |
| 106 | + | } | |
| 107 | + | if sigv4 != nil { | |
| 108 | + | c.sigv4 = sigv4 | |
| 109 | + | return c | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | return &unimplementedClient{ | |
| 113 | + | name: "amazon-bedrock", | |
| 114 | + | hint: "no Bedrock credentials found (set AWS_BEARER_TOKEN_BEDROCK, AWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEY, or AWS_PROFILE)", | |
| 115 | + | } | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | // bedrockResolveRegion returns the AWS region for Bedrock calls, | |
| 119 | + | // honoring AWS_REGION then AWS_DEFAULT_REGION, defaulting to us-east-1. | |
| 120 | + | func bedrockResolveRegion() string { | |
| 121 | + | region := os.Getenv("AWS_REGION") | |
| 122 | + | if region == "" { | |
| 123 | + | region = os.Getenv("AWS_DEFAULT_REGION") | |
| 124 | + | } | |
| 125 | + | if region == "" { | |
| 126 | + | region = "us-east-1" | |
| 127 | + | } | |
| 128 | + | return region | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | // resolveBedrockAuth determines Bedrock credentials from the same | |
| 132 | + | // sources NewBedrockClient uses, so discovery and inference share one | |
| 133 | + | // resolution path. Exactly one of (bearer, sigv4) is non-empty on | |
| 134 | + | // success; both are zero when no credentials are found. | |
| 135 | + | // | |
| 136 | + | // Resolution order (first match wins): explicit apiKey bearer, then | |
| 137 | + | // AWS_BEARER_TOKEN_BEDROCK, then AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY | |
| 138 | + | // env vars, then ~/.aws/credentials via AWS_PROFILE. | |
| 139 | + | func resolveBedrockAuth(apiKey string) (bearer string, sigv4 *bedrockSigV4Creds) { | |
| 106 | 140 | token := apiKey | |
| 107 | 141 | if token == "" || token == "<aws>" { | |
| 108 | 142 | token = os.Getenv("AWS_BEARER_TOKEN_BEDROCK") | |
| 109 | 143 | } | |
| 110 | 144 | if token != "" && token != "<aws>" { | |
| 111 | - | c.bearerToken = token | |
| 112 | - | return c | |
| 145 | + | return token, nil | |
| 113 | 146 | } | |
| 114 | 147 | ||
| 115 | - | // SigV4 route: env vars first. | |
| 148 | + | return "", resolveBedrockDiscoveryCreds() | |
| 149 | + | } | |
| 150 | + | ||
| 151 | + | // resolveBedrockDiscoveryCreds ignores inference bearer tokens: Bedrock's | |
| 152 | + | // control-plane listing endpoints require SigV4 even when inference uses a bearer. | |
| 153 | + | func resolveBedrockDiscoveryCreds() *bedrockSigV4Creds { | |
| 116 | 154 | ak := os.Getenv("AWS_ACCESS_KEY_ID") | |
| 117 | 155 | sk := os.Getenv("AWS_SECRET_ACCESS_KEY") | |
| 118 | 156 | st := os.Getenv("AWS_SESSION_TOKEN") | |
| 119 | 157 | if ak != "" && sk != "" { | |
| 120 | - | c.sigv4 = &bedrockSigV4Creds{accessKeyID: ak, secretAccessKey: sk, sessionToken: st} | |
| 121 | - | return c | |
| 158 | + | return &bedrockSigV4Creds{accessKeyID: ak, secretAccessKey: sk, sessionToken: st} | |
| 122 | 159 | } | |
| 123 | 160 | ||
| 124 | - | // SigV4 route: ~/.aws/credentials via AWS_PROFILE. | |
| 125 | 161 | if profile := os.Getenv("AWS_PROFILE"); profile != "" { | |
| 126 | 162 | if creds, err := readAWSCredentialsFile(profile); err == nil { | |
| 127 | - | c.sigv4 = creds | |
| 128 | - | return c | |
| 163 | + | return creds | |
| 129 | 164 | } | |
| 130 | 165 | } | |
| 131 | 166 | ||
| 132 | - | return &unimplementedClient{ | |
| 133 | - | name: "amazon-bedrock", | |
| 134 | - | hint: "no Bedrock credentials found (set AWS_BEARER_TOKEN_BEDROCK, AWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEY, or AWS_PROFILE)", | |
| 167 | + | // Last resort: ask the AWS CLI to resolve credentials for the active | |
| 168 | + | // profile. This handles SSO, assumed roles, credential_process, and | |
| 169 | + | // web-identity setups whose credentials are not stored as static keys | |
| 170 | + | // in ~/.aws/credentials. Best-effort: absent CLI or an expired login | |
| 171 | + | // simply yields no credentials. | |
| 172 | + | if creds := resolveBedrockCredsViaCLI(); creds != nil { | |
| 173 | + | return creds | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | return nil | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | // resolveBedrockCredsViaCLI shells out to `aws configure | |
| 180 | + | // export-credentials --format process`, which returns fully-resolved | |
| 181 | + | // credentials for the active profile regardless of how that profile is | |
| 182 | + | // configured (static keys, SSO, assume-role, credential_process). The | |
| 183 | + | // AWS CLI must be installed and the profile's login (if any) still | |
| 184 | + | // valid. Returns nil on any failure so the caller can fall through. | |
| 185 | + | func resolveBedrockCredsViaCLI() *bedrockSigV4Creds { | |
| 186 | + | if _, err := exec.LookPath("aws"); err != nil { | |
| 187 | + | return nil | |
| 188 | + | } | |
| 189 | + | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) | |
| 190 | + | defer cancel() | |
| 191 | + | // AWS_PROFILE / AWS_REGION already in the environment are inherited, | |
| 192 | + | // so the CLI resolves the same profile zot was pointed at. | |
| 193 | + | cmd := exec.CommandContext(ctx, "aws", "configure", "export-credentials", "--format", "process") | |
| 194 | + | out, err := cmd.Output() | |
| 195 | + | if err != nil { | |
| 196 | + | return nil | |
| 197 | + | } | |
| 198 | + | var exported struct { | |
| 199 | + | AccessKeyID string `json:"AccessKeyId"` | |
| 200 | + | SecretAccessKey string `json:"SecretAccessKey"` | |
| 201 | + | SessionToken string `json:"SessionToken"` | |
| 202 | + | } | |
| 203 | + | if err := json.Unmarshal(out, &exported); err != nil { | |
| 204 | + | return nil | |
| 205 | + | } | |
| 206 | + | if exported.AccessKeyID == "" || exported.SecretAccessKey == "" { | |
| 207 | + | return nil | |
| 208 | + | } | |
| 209 | + | return &bedrockSigV4Creds{ | |
| 210 | + | accessKeyID: exported.AccessKeyID, | |
| 211 | + | secretAccessKey: exported.SecretAccessKey, | |
| 212 | + | sessionToken: exported.SessionToken, | |
| 135 | 213 | } | |
| 136 | 214 | } | |
| 137 | 215 | ||
| @@ -50,6 +50,7 @@ import ( | |||||
| 50 | "net/http" | 50 | "net/http" | ||
| 51 | "net/url" | 51 | "net/url" | ||
| 52 | "os" | 52 | "os" | ||
| 53 | + | "os/exec" | |||
| 53 | "sort" | 54 | "sort" | ||
| 54 | "strings" | 55 | "strings" | ||
| 55 | "time" | 56 | "time" | ||
| @@ -82,17 +83,13 @@ type bedrockSigV4Creds struct { | |||||
| 82 | // 3. AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY (+ optional | 83 | // 3. AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY (+ optional | ||
| 83 | // AWS_SESSION_TOKEN) -> SigV4 route. | 84 | // AWS_SESSION_TOKEN) -> SigV4 route. | ||
| 84 | // 4. AWS_PROFILE -> read ~/.aws/credentials, take that profile's keys. | 85 | // 4. AWS_PROFILE -> read ~/.aws/credentials, take that profile's keys. | ||
| 86 | + | // 5. AWS_PROFILE -> `aws configure export-credentials` (SSO, assume-role, | |||
| 87 | + | // credential_process) when the profile has no static keys. | |||
| 85 | // | 88 | // | ||
| 86 | // region defaults to us-east-1 unless AWS_REGION / AWS_DEFAULT_REGION is | 89 | // region defaults to us-east-1 unless AWS_REGION / AWS_DEFAULT_REGION is | ||
| 87 | // set or the baseURL embeds a region. | 90 | // set or the baseURL embeds a region. | ||
| 88 | func NewBedrockClient(apiKey, baseURL string) Client { | 91 | func NewBedrockClient(apiKey, baseURL string) Client { | ||
| 89 | - | region := os.Getenv("AWS_REGION") | 92 | + | region := bedrockResolveRegion() |
| 90 | - | if region == "" { | |||
| 91 | - | region = os.Getenv("AWS_DEFAULT_REGION") | |||
| 92 | - | } | |||
| 93 | - | if region == "" { | |||
| 94 | - | region = "us-east-1" | |||
| 95 | - | } | |||
| 96 | if baseURL == "" { | 93 | if baseURL == "" { | ||
| 97 | baseURL = "https://bedrock-runtime." + region + ".amazonaws.com" | 94 | baseURL = "https://bedrock-runtime." + region + ".amazonaws.com" | ||
| 98 | } | 95 | } | ||
| @@ -102,36 +99,117 @@ func NewBedrockClient(apiKey, baseURL string) Client { | |||||
| 102 | http: &http.Client{Timeout: 0}, | 99 | http: &http.Client{Timeout: 0}, | ||
| 103 | } | 100 | } | ||
| 104 | 101 | ||||
| 105 | - | // Bearer route. | 102 | + | bearer, sigv4 := resolveBedrockAuth(apiKey) |
| 103 | + | if bearer != "" { | |||
| 104 | + | c.bearerToken = bearer | |||
| 105 | + | return c | |||
| 106 | + | } | |||
| 107 | + | if sigv4 != nil { | |||
| 108 | + | c.sigv4 = sigv4 | |||
| 109 | + | return c | |||
| 110 | + | } | |||
| 111 | + | ||||
| 112 | + | return &unimplementedClient{ | |||
| 113 | + | name: "amazon-bedrock", | |||
| 114 | + | hint: "no Bedrock credentials found (set AWS_BEARER_TOKEN_BEDROCK, AWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEY, or AWS_PROFILE)", | |||
| 115 | + | } | |||
| 116 | + | } | |||
| 117 | + | ||||
| 118 | + | // bedrockResolveRegion returns the AWS region for Bedrock calls, | |||
| 119 | + | // honoring AWS_REGION then AWS_DEFAULT_REGION, defaulting to us-east-1. | |||
| 120 | + | func bedrockResolveRegion() string { | |||
| 121 | + | region := os.Getenv("AWS_REGION") | |||
| 122 | + | if region == "" { | |||
| 123 | + | region = os.Getenv("AWS_DEFAULT_REGION") | |||
| 124 | + | } | |||
| 125 | + | if region == "" { | |||
| 126 | + | region = "us-east-1" | |||
| 127 | + | } | |||
| 128 | + | return region | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | // resolveBedrockAuth determines Bedrock credentials from the same | |||
| 132 | + | // sources NewBedrockClient uses, so discovery and inference share one | |||
| 133 | + | // resolution path. Exactly one of (bearer, sigv4) is non-empty on | |||
| 134 | + | // success; both are zero when no credentials are found. | |||
| 135 | + | // | |||
| 136 | + | // Resolution order (first match wins): explicit apiKey bearer, then | |||
| 137 | + | // AWS_BEARER_TOKEN_BEDROCK, then AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY | |||
| 138 | + | // env vars, then ~/.aws/credentials via AWS_PROFILE. | |||
| 139 | + | func resolveBedrockAuth(apiKey string) (bearer string, sigv4 *bedrockSigV4Creds) { | |||
| 106 | token := apiKey | 140 | token := apiKey | ||
| 107 | if token == "" || token == "<aws>" { | 141 | if token == "" || token == "<aws>" { | ||
| 108 | token = os.Getenv("AWS_BEARER_TOKEN_BEDROCK") | 142 | token = os.Getenv("AWS_BEARER_TOKEN_BEDROCK") | ||
| 109 | } | 143 | } | ||
| 110 | if token != "" && token != "<aws>" { | 144 | if token != "" && token != "<aws>" { | ||
| 111 | - | c.bearerToken = token | 145 | + | return token, nil |
| 112 | - | return c | |||
| 113 | } | 146 | } | ||
| 114 | 147 | ||||
| 115 | - | // SigV4 route: env vars first. | 148 | + | return "", resolveBedrockDiscoveryCreds() |
| 149 | + | } | |||
| 150 | + | ||||
| 151 | + | // resolveBedrockDiscoveryCreds ignores inference bearer tokens: Bedrock's | |||
| 152 | + | // control-plane listing endpoints require SigV4 even when inference uses a bearer. | |||
| 153 | + | func resolveBedrockDiscoveryCreds() *bedrockSigV4Creds { | |||
| 116 | ak := os.Getenv("AWS_ACCESS_KEY_ID") | 154 | ak := os.Getenv("AWS_ACCESS_KEY_ID") | ||
| 117 | sk := os.Getenv("AWS_SECRET_ACCESS_KEY") | 155 | sk := os.Getenv("AWS_SECRET_ACCESS_KEY") | ||
| 118 | st := os.Getenv("AWS_SESSION_TOKEN") | 156 | st := os.Getenv("AWS_SESSION_TOKEN") | ||
| 119 | if ak != "" && sk != "" { | 157 | if ak != "" && sk != "" { | ||
| 120 | - | c.sigv4 = &bedrockSigV4Creds{accessKeyID: ak, secretAccessKey: sk, sessionToken: st} | 158 | + | return &bedrockSigV4Creds{accessKeyID: ak, secretAccessKey: sk, sessionToken: st} |
| 121 | - | return c | |||
| 122 | } | 159 | } | ||
| 123 | 160 | ||||
| 124 | - | // SigV4 route: ~/.aws/credentials via AWS_PROFILE. | |||
| 125 | if profile := os.Getenv("AWS_PROFILE"); profile != "" { | 161 | if profile := os.Getenv("AWS_PROFILE"); profile != "" { | ||
| 126 | if creds, err := readAWSCredentialsFile(profile); err == nil { | 162 | if creds, err := readAWSCredentialsFile(profile); err == nil { | ||
| 127 | - | c.sigv4 = creds | 163 | + | return creds |
| 128 | - | return c | |||
| 129 | } | 164 | } | ||
| 130 | } | 165 | } | ||
| 131 | 166 | ||||
| 132 | - | return &unimplementedClient{ | 167 | + | // Last resort: ask the AWS CLI to resolve credentials for the active |
| 133 | - | name: "amazon-bedrock", | 168 | + | // profile. This handles SSO, assumed roles, credential_process, and |
| 134 | - | hint: "no Bedrock credentials found (set AWS_BEARER_TOKEN_BEDROCK, AWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEY, or AWS_PROFILE)", | 169 | + | // web-identity setups whose credentials are not stored as static keys |
| 170 | + | // in ~/.aws/credentials. Best-effort: absent CLI or an expired login | |||
| 171 | + | // simply yields no credentials. | |||
| 172 | + | if creds := resolveBedrockCredsViaCLI(); creds != nil { | |||
| 173 | + | return creds | |||
| 174 | + | } | |||
| 175 | + | ||||
| 176 | + | return nil | |||
| 177 | + | } | |||
| 178 | + | ||||
| 179 | + | // resolveBedrockCredsViaCLI shells out to `aws configure | |||
| 180 | + | // export-credentials --format process`, which returns fully-resolved | |||
| 181 | + | // credentials for the active profile regardless of how that profile is | |||
| 182 | + | // configured (static keys, SSO, assume-role, credential_process). The | |||
| 183 | + | // AWS CLI must be installed and the profile's login (if any) still | |||
| 184 | + | // valid. Returns nil on any failure so the caller can fall through. | |||
| 185 | + | func resolveBedrockCredsViaCLI() *bedrockSigV4Creds { | |||
| 186 | + | if _, err := exec.LookPath("aws"); err != nil { | |||
| 187 | + | return nil | |||
| 188 | + | } | |||
| 189 | + | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) | |||
| 190 | + | defer cancel() | |||
| 191 | + | // AWS_PROFILE / AWS_REGION already in the environment are inherited, | |||
| 192 | + | // so the CLI resolves the same profile zot was pointed at. | |||
| 193 | + | cmd := exec.CommandContext(ctx, "aws", "configure", "export-credentials", "--format", "process") | |||
| 194 | + | out, err := cmd.Output() | |||
| 195 | + | if err != nil { | |||
| 196 | + | return nil | |||
| 197 | + | } | |||
| 198 | + | var exported struct { | |||
| 199 | + | AccessKeyID string `json:"AccessKeyId"` | |||
| 200 | + | SecretAccessKey string `json:"SecretAccessKey"` | |||
| 201 | + | SessionToken string `json:"SessionToken"` | |||
| 202 | + | } | |||
| 203 | + | if err := json.Unmarshal(out, &exported); err != nil { | |||
| 204 | + | return nil | |||
| 205 | + | } | |||
| 206 | + | if exported.AccessKeyID == "" || exported.SecretAccessKey == "" { | |||
| 207 | + | return nil | |||
| 208 | + | } | |||
| 209 | + | return &bedrockSigV4Creds{ | |||
| 210 | + | accessKeyID: exported.AccessKeyID, | |||
| 211 | + | secretAccessKey: exported.SecretAccessKey, | |||
| 212 | + | sessionToken: exported.SessionToken, | |||
| 135 | } | 213 | } | ||
| 136 | } | 214 | } | ||
| 137 | 215 | ||||
M packages/provider/catalog_builtin.go +9 -3
| @@ -34,14 +34,16 @@ var builtinCatalog = []Model{ | |||
| 34 | 34 | {Provider: "amazon-bedrock", ID: "anthropic.claude-opus-4-6-v1", DisplayName: "Claude Opus 4.6", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 35 | 35 | {Provider: "amazon-bedrock", ID: "anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 36 | 36 | {Provider: "amazon-bedrock", ID: "anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 37 | + | {Provider: "amazon-bedrock", ID: "anthropic.claude-opus-5-5", DisplayName: "Claude Opus 5.5", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 37 | 38 | {Provider: "amazon-bedrock", ID: "anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 38 | 39 | {Provider: "amazon-bedrock", ID: "anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 39 | 40 | {Provider: "amazon-bedrock", ID: "anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 40 | 41 | {Provider: "amazon-bedrock", ID: "au.anthropic.claude-haiku-4-5-20251001-v1:0", DisplayName: "Claude Haiku 4.5 (AU)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 1, PriceOutput: 5, PriceCacheRead: 0.1, PriceCacheWrite: 1.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 41 | - | {Provider: "amazon-bedrock", ID: "au.anthropic.claude-opus-4-6-v1", DisplayName: "AU Anthropic Claude Opus 4.6", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 16.5, PriceOutput: 82.5, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 42 | - | {Provider: "amazon-bedrock", ID: "au.anthropic.claude-opus-4-8", DisplayName: "AU Anthropic Claude Opus 4.8", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 16.5, PriceOutput: 82.5, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 42 | + | {Provider: "amazon-bedrock", ID: "au.anthropic.claude-opus-4-6-v1", DisplayName: "AU Anthropic Claude Opus 4.6", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 43 | + | {Provider: "amazon-bedrock", ID: "au.anthropic.claude-opus-4-8", DisplayName: "AU Anthropic Claude Opus 4.8", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 44 | + | {Provider: "amazon-bedrock", ID: "au.anthropic.claude-opus-5-5", DisplayName: "Claude Opus 5.5 (AU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 43 | 45 | {Provider: "amazon-bedrock", ID: "au.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (AU)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 44 | - | {Provider: "amazon-bedrock", ID: "au.anthropic.claude-sonnet-4-6", DisplayName: "AU Anthropic Claude Sonnet 4.6", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 3.3, PriceOutput: 16.5, PriceCacheRead: 0.33, PriceCacheWrite: 4.125, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 46 | + | {Provider: "amazon-bedrock", ID: "au.anthropic.claude-sonnet-4-6", DisplayName: "AU Anthropic Claude Sonnet 4.6", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 45 | 47 | {Provider: "amazon-bedrock", ID: "au.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (AU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 46 | 48 | {Provider: "amazon-bedrock", ID: "deepseek.r1-v1:0", DisplayName: "DeepSeek-R1", ContextWindow: 128000, MaxOutput: 32768, Reasoning: true, PriceInput: 1.35, PriceOutput: 5.4, PriceCacheRead: 0, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 47 | 49 | {Provider: "amazon-bedrock", ID: "deepseek.v3-v1:0", DisplayName: "DeepSeek-V3.1", ContextWindow: 163840, MaxOutput: 81920, Reasoning: true, PriceInput: 0.58, PriceOutput: 1.68, PriceCacheRead: 0, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| @@ -52,6 +54,7 @@ var builtinCatalog = []Model{ | |||
| 52 | 54 | {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-opus-4-6-v1", DisplayName: "Claude Opus 4.6 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"}, | |
| 53 | 55 | {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"}, | |
| 54 | 56 | {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"}, | |
| 57 | + | {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-opus-5-5", DisplayName: "Claude Opus 5.5 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"}, | |
| 55 | 58 | {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (EU)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"}, | |
| 56 | 59 | {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6 (EU)", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"}, | |
| 57 | 60 | {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"}, | |
| @@ -62,6 +65,7 @@ var builtinCatalog = []Model{ | |||
| 62 | 65 | {Provider: "amazon-bedrock", ID: "global.anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 63 | 66 | {Provider: "amazon-bedrock", ID: "global.anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 64 | 67 | {Provider: "amazon-bedrock", ID: "global.anthropic.claude-opus-5", DisplayName: "Claude Opus 5 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 68 | + | {Provider: "amazon-bedrock", ID: "global.anthropic.claude-opus-5-5", DisplayName: "Claude Opus 5.5 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 65 | 69 | {Provider: "amazon-bedrock", ID: "global.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (Global)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 66 | 70 | {Provider: "amazon-bedrock", ID: "global.anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6 (Global)", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 67 | 71 | {Provider: "amazon-bedrock", ID: "global.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| @@ -569,6 +573,7 @@ var builtinCatalog = []Model{ | |||
| 569 | 573 | {Provider: "amazon-bedrock", ID: "google.gemma-3-4b-it", DisplayName: "Gemma 3 4B IT", ContextWindow: 128000, MaxOutput: 4096, Reasoning: false, PriceInput: 0.04, PriceOutput: 0.08, PriceCacheRead: 0, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 570 | 574 | {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7 (JP)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 571 | 575 | {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8 (JP)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 576 | + | {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-opus-5-5", DisplayName: "Claude Opus 5.5 (JP)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 572 | 577 | {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (JP)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 573 | 578 | {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6 (JP)", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 574 | 579 | {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (JP)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| @@ -613,6 +618,7 @@ var builtinCatalog = []Model{ | |||
| 613 | 618 | {Provider: "amazon-bedrock", ID: "us.anthropic.claude-opus-4-6-v1", DisplayName: "Claude Opus 4.6 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 614 | 619 | {Provider: "amazon-bedrock", ID: "us.anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 615 | 620 | {Provider: "amazon-bedrock", ID: "us.anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 621 | + | {Provider: "amazon-bedrock", ID: "us.anthropic.claude-opus-5-5", DisplayName: "Claude Opus 5.5 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 616 | 622 | {Provider: "amazon-bedrock", ID: "us.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (US)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 617 | 623 | {Provider: "amazon-bedrock", ID: "us.anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6 (US)", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| 618 | 624 | {Provider: "amazon-bedrock", ID: "us.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |
| @@ -34,14 +34,16 @@ var builtinCatalog = []Model{ | |||||
| 34 | {Provider: "amazon-bedrock", ID: "anthropic.claude-opus-4-6-v1", DisplayName: "Claude Opus 4.6", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 34 | {Provider: "amazon-bedrock", ID: "anthropic.claude-opus-4-6-v1", DisplayName: "Claude Opus 4.6", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| 35 | {Provider: "amazon-bedrock", ID: "anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 35 | {Provider: "amazon-bedrock", ID: "anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| 36 | {Provider: "amazon-bedrock", ID: "anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 36 | {Provider: "amazon-bedrock", ID: "anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| 37 | + | {Provider: "amazon-bedrock", ID: "anthropic.claude-opus-5-5", DisplayName: "Claude Opus 5.5", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |||
| 37 | {Provider: "amazon-bedrock", ID: "anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 38 | {Provider: "amazon-bedrock", ID: "anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| 38 | {Provider: "amazon-bedrock", ID: "anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 39 | {Provider: "amazon-bedrock", ID: "anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| 39 | {Provider: "amazon-bedrock", ID: "anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 40 | {Provider: "amazon-bedrock", ID: "anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| 40 | {Provider: "amazon-bedrock", ID: "au.anthropic.claude-haiku-4-5-20251001-v1:0", DisplayName: "Claude Haiku 4.5 (AU)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 1, PriceOutput: 5, PriceCacheRead: 0.1, PriceCacheWrite: 1.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 41 | {Provider: "amazon-bedrock", ID: "au.anthropic.claude-haiku-4-5-20251001-v1:0", DisplayName: "Claude Haiku 4.5 (AU)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 1, PriceOutput: 5, PriceCacheRead: 0.1, PriceCacheWrite: 1.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| 41 | - | {Provider: "amazon-bedrock", ID: "au.anthropic.claude-opus-4-6-v1", DisplayName: "AU Anthropic Claude Opus 4.6", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 16.5, PriceOutput: 82.5, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 42 | + | {Provider: "amazon-bedrock", ID: "au.anthropic.claude-opus-4-6-v1", DisplayName: "AU Anthropic Claude Opus 4.6", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, |
| 42 | - | {Provider: "amazon-bedrock", ID: "au.anthropic.claude-opus-4-8", DisplayName: "AU Anthropic Claude Opus 4.8", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 16.5, PriceOutput: 82.5, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 43 | + | {Provider: "amazon-bedrock", ID: "au.anthropic.claude-opus-4-8", DisplayName: "AU Anthropic Claude Opus 4.8", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, |
| 44 | + | {Provider: "amazon-bedrock", ID: "au.anthropic.claude-opus-5-5", DisplayName: "Claude Opus 5.5 (AU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |||
| 43 | {Provider: "amazon-bedrock", ID: "au.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (AU)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 45 | {Provider: "amazon-bedrock", ID: "au.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (AU)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| 44 | - | {Provider: "amazon-bedrock", ID: "au.anthropic.claude-sonnet-4-6", DisplayName: "AU Anthropic Claude Sonnet 4.6", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 3.3, PriceOutput: 16.5, PriceCacheRead: 0.33, PriceCacheWrite: 4.125, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 46 | + | {Provider: "amazon-bedrock", ID: "au.anthropic.claude-sonnet-4-6", DisplayName: "AU Anthropic Claude Sonnet 4.6", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, |
| 45 | {Provider: "amazon-bedrock", ID: "au.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (AU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 47 | {Provider: "amazon-bedrock", ID: "au.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (AU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| 46 | {Provider: "amazon-bedrock", ID: "deepseek.r1-v1:0", DisplayName: "DeepSeek-R1", ContextWindow: 128000, MaxOutput: 32768, Reasoning: true, PriceInput: 1.35, PriceOutput: 5.4, PriceCacheRead: 0, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 48 | {Provider: "amazon-bedrock", ID: "deepseek.r1-v1:0", DisplayName: "DeepSeek-R1", ContextWindow: 128000, MaxOutput: 32768, Reasoning: true, PriceInput: 1.35, PriceOutput: 5.4, PriceCacheRead: 0, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| 47 | {Provider: "amazon-bedrock", ID: "deepseek.v3-v1:0", DisplayName: "DeepSeek-V3.1", ContextWindow: 163840, MaxOutput: 81920, Reasoning: true, PriceInput: 0.58, PriceOutput: 1.68, PriceCacheRead: 0, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 49 | {Provider: "amazon-bedrock", ID: "deepseek.v3-v1:0", DisplayName: "DeepSeek-V3.1", ContextWindow: 163840, MaxOutput: 81920, Reasoning: true, PriceInput: 0.58, PriceOutput: 1.68, PriceCacheRead: 0, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| @@ -52,6 +54,7 @@ var builtinCatalog = []Model{ | |||||
| 52 | {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-opus-4-6-v1", DisplayName: "Claude Opus 4.6 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"}, | 54 | {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-opus-4-6-v1", DisplayName: "Claude Opus 4.6 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"}, | ||
| 53 | {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"}, | 55 | {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"}, | ||
| 54 | {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"}, | 56 | {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"}, | ||
| 57 | + | {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-opus-5-5", DisplayName: "Claude Opus 5.5 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"}, | |||
| 55 | {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (EU)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"}, | 58 | {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (EU)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"}, | ||
| 56 | {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6 (EU)", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"}, | 59 | {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6 (EU)", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"}, | ||
| 57 | {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"}, | 60 | {Provider: "amazon-bedrock", ID: "eu.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (EU)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.eu-central-1.amazonaws.com"}, | ||
| @@ -62,6 +65,7 @@ var builtinCatalog = []Model{ | |||||
| 62 | {Provider: "amazon-bedrock", ID: "global.anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 65 | {Provider: "amazon-bedrock", ID: "global.anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| 63 | {Provider: "amazon-bedrock", ID: "global.anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 66 | {Provider: "amazon-bedrock", ID: "global.anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| 64 | {Provider: "amazon-bedrock", ID: "global.anthropic.claude-opus-5", DisplayName: "Claude Opus 5 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 67 | {Provider: "amazon-bedrock", ID: "global.anthropic.claude-opus-5", DisplayName: "Claude Opus 5 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| 68 | + | {Provider: "amazon-bedrock", ID: "global.anthropic.claude-opus-5-5", DisplayName: "Claude Opus 5.5 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |||
| 65 | {Provider: "amazon-bedrock", ID: "global.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (Global)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 69 | {Provider: "amazon-bedrock", ID: "global.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (Global)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| 66 | {Provider: "amazon-bedrock", ID: "global.anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6 (Global)", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 70 | {Provider: "amazon-bedrock", ID: "global.anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6 (Global)", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| 67 | {Provider: "amazon-bedrock", ID: "global.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 71 | {Provider: "amazon-bedrock", ID: "global.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (Global)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| @@ -569,6 +573,7 @@ var builtinCatalog = []Model{ | |||||
| 569 | {Provider: "amazon-bedrock", ID: "google.gemma-3-4b-it", DisplayName: "Gemma 3 4B IT", ContextWindow: 128000, MaxOutput: 4096, Reasoning: false, PriceInput: 0.04, PriceOutput: 0.08, PriceCacheRead: 0, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 573 | {Provider: "amazon-bedrock", ID: "google.gemma-3-4b-it", DisplayName: "Gemma 3 4B IT", ContextWindow: 128000, MaxOutput: 4096, Reasoning: false, PriceInput: 0.04, PriceOutput: 0.08, PriceCacheRead: 0, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| 570 | {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7 (JP)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 574 | {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7 (JP)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| 571 | {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8 (JP)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 575 | {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8 (JP)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| 576 | + | {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-opus-5-5", DisplayName: "Claude Opus 5.5 (JP)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |||
| 572 | {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (JP)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 577 | {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (JP)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| 573 | {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6 (JP)", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 578 | {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6 (JP)", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| 574 | {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (JP)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 579 | {Provider: "amazon-bedrock", ID: "jp.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (JP)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| @@ -613,6 +618,7 @@ var builtinCatalog = []Model{ | |||||
| 613 | {Provider: "amazon-bedrock", ID: "us.anthropic.claude-opus-4-6-v1", DisplayName: "Claude Opus 4.6 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 618 | {Provider: "amazon-bedrock", ID: "us.anthropic.claude-opus-4-6-v1", DisplayName: "Claude Opus 4.6 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| 614 | {Provider: "amazon-bedrock", ID: "us.anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 619 | {Provider: "amazon-bedrock", ID: "us.anthropic.claude-opus-4-7", DisplayName: "Claude Opus 4.7 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| 615 | {Provider: "amazon-bedrock", ID: "us.anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 620 | {Provider: "amazon-bedrock", ID: "us.anthropic.claude-opus-4-8", DisplayName: "Claude Opus 4.8 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, PriceInput: 5, PriceOutput: 25, PriceCacheRead: 0.5, PriceCacheWrite: 6.25, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| 621 | + | {Provider: "amazon-bedrock", ID: "us.anthropic.claude-opus-5-5", DisplayName: "Claude Opus 5.5 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 4, PriceOutput: 20, PriceCacheRead: 0.2, PriceCacheWrite: 5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | |||
| 616 | {Provider: "amazon-bedrock", ID: "us.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (US)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 622 | {Provider: "amazon-bedrock", ID: "us.anthropic.claude-sonnet-4-5-20250929-v1:0", DisplayName: "Claude Sonnet 4.5 (US)", ContextWindow: 200000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| 617 | {Provider: "amazon-bedrock", ID: "us.anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6 (US)", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 623 | {Provider: "amazon-bedrock", ID: "us.anthropic.claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6 (US)", ContextWindow: 1000000, MaxOutput: 64000, Reasoning: true, PriceInput: 3, PriceOutput: 15, PriceCacheRead: 0.3, PriceCacheWrite: 3.75, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
| 618 | {Provider: "amazon-bedrock", ID: "us.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | 624 | {Provider: "amazon-bedrock", ID: "us.anthropic.claude-sonnet-5", DisplayName: "Claude Sonnet 5 (US)", ContextWindow: 1000000, MaxOutput: 128000, Reasoning: true, AdaptiveThinking: true, PriceInput: 2, PriceOutput: 10, PriceCacheRead: 0.2, PriceCacheWrite: 2.5, BaseURL: "https://bedrock-runtime.us-east-1.amazonaws.com"}, | ||
A packages/provider/discover_bedrock.go +203 -0
| @@ -0,0 +1,203 @@ | |||
| 1 | + | package 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 | + | ||
| 31 | + | import ( | |
| 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. | |
| 45 | + | func 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. | |
| 61 | + | func 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. | |
| 98 | + | func 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 | + | ||
| 167 | + | func 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. | |
| 182 | + | func 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 | + | } | |
| @@ -0,0 +1,203 @@ | |||||
| 1 | + | package 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 | + | ||||
| 31 | + | import ( | |||
| 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. | |||
| 45 | + | func 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. | |||
| 61 | + | func 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. | |||
| 98 | + | func 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 | + | ||||
| 167 | + | func 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. | |||
| 182 | + | func 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 | + | } | |||
A packages/provider/discover_bedrock_capture_live_test.go +230 -0
| @@ -0,0 +1,230 @@ | |||
| 1 | + | package provider | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "encoding/json" | |
| 6 | + | "net/http" | |
| 7 | + | "os" | |
| 8 | + | "strings" | |
| 9 | + | "testing" | |
| 10 | + | "time" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // Live Bedrock discovery diagnostic / capture helper. | |
| 14 | + | // | |
| 15 | + | // This is NOT a validation test. It lets a developer with real AWS | |
| 16 | + | // credentials capture actual control-plane and Price List responses to | |
| 17 | + | // verify discovery output and re-check the pricing situation. It never | |
| 18 | + | // runs in `go test ./...` unless explicitly enabled, so CI and unpaid | |
| 19 | + | // checkouts never hit the network. | |
| 20 | + | // | |
| 21 | + | // Background: a live capture (Sep 2026, us-east-1) confirmed the AWS | |
| 22 | + | // Price List API returns only stale, input-only rows for legacy Anthropic | |
| 23 | + | // models (Claude 2.x / 3 Sonnet / 3 Haiku) and no output/cache prices for | |
| 24 | + | // any current model, so zot does NOT enrich pricing from it — the catalog | |
| 25 | + | // stays authoritative. This helper is retained to detect if/when AWS | |
| 26 | + | // fixes that, and to confirm ListFoundationModels / ListInferenceProfiles | |
| 27 | + | // shapes when they change. | |
| 28 | + | // | |
| 29 | + | // Enable and run: | |
| 30 | + | // | |
| 31 | + | // ZOT_BEDROCK_CAPTURE=1 \ | |
| 32 | + | // AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... [AWS_SESSION_TOKEN=...] \ | |
| 33 | + | // AWS_REGION=us-east-1 \ | |
| 34 | + | // go test ./packages/provider -run TestBedrockCaptureLive -v | |
| 35 | + | // | |
| 36 | + | // Or with a profile: | |
| 37 | + | // | |
| 38 | + | // ZOT_BEDROCK_CAPTURE=1 AWS_PROFILE=my-sso AWS_REGION=us-east-1 \ | |
| 39 | + | // go test ./packages/provider -run TestBedrockCaptureLive -v | |
| 40 | + | // | |
| 41 | + | // Output is written to files under $ZOT_BEDROCK_CAPTURE_DIR (default: a | |
| 42 | + | // temp dir printed in the log). Secrets are never captured — only the | |
| 43 | + | // public model/pricing JSON. | |
| 44 | + | // | |
| 45 | + | // Requires the caller's IAM principal to allow: | |
| 46 | + | // - bedrock:ListFoundationModels | |
| 47 | + | // - bedrock:ListInferenceProfiles | |
| 48 | + | // - pricing:GetProducts | |
| 49 | + | func TestBedrockCaptureLive(t *testing.T) { | |
| 50 | + | if strings.TrimSpace(os.Getenv("ZOT_BEDROCK_CAPTURE")) == "" { | |
| 51 | + | t.Skip("set ZOT_BEDROCK_CAPTURE=1 (with AWS creds) to capture live Bedrock responses") | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | _, sigv4 := resolveBedrockAuth("") | |
| 55 | + | if sigv4 == nil { | |
| 56 | + | t.Fatal("no SigV4 credentials found; set AWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEY or AWS_PROFILE") | |
| 57 | + | } | |
| 58 | + | region := bedrockResolveRegion() | |
| 59 | + | t.Logf("capturing Bedrock responses for region=%s", region) | |
| 60 | + | ||
| 61 | + | outDir := strings.TrimSpace(os.Getenv("ZOT_BEDROCK_CAPTURE_DIR")) | |
| 62 | + | if outDir == "" { | |
| 63 | + | outDir = t.TempDir() | |
| 64 | + | } | |
| 65 | + | t.Logf("writing captures to %s", outDir) | |
| 66 | + | ||
| 67 | + | client := &http.Client{Timeout: 30 * time.Second} | |
| 68 | + | ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) | |
| 69 | + | defer cancel() | |
| 70 | + | ||
| 71 | + | // 1. ListFoundationModels (control plane, GET). | |
| 72 | + | fmURL := "https://bedrock." + region + ".amazonaws.com/foundation-models" | |
| 73 | + | if body, err := bedrockSignedControlPlane(ctx, client, sigv4, region, http.MethodGet, fmURL, nil); err != nil { | |
| 74 | + | t.Errorf("ListFoundationModels failed: %v", err) | |
| 75 | + | } else { | |
| 76 | + | writeCapture(t, outDir, "foundation-models.json", body) | |
| 77 | + | // Also log the summarized model IDs so the capturer can eyeball them. | |
| 78 | + | logBedrockModelIDs(t, body) | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | // 2. ListInferenceProfiles (control plane, GET, paginated). | |
| 82 | + | ipURL := "https://bedrock." + region + ".amazonaws.com/inference-profiles?maxResults=1000" | |
| 83 | + | if body, err := bedrockSignedControlPlane(ctx, client, sigv4, region, http.MethodGet, ipURL, nil); err != nil { | |
| 84 | + | t.Errorf("ListInferenceProfiles failed: %v", err) | |
| 85 | + | } else { | |
| 86 | + | writeCapture(t, outDir, "inference-profiles.json", body) | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | // 3. GetProducts for AmazonBedrock (Price List API, POST). | |
| 90 | + | // Capture the FIRST page raw so we can inspect real attribute | |
| 91 | + | // names, inferenceType values, feature strings, and units. We | |
| 92 | + | // deliberately do not paginate here — one page is enough to fix | |
| 93 | + | // the fixtures, and it keeps the captured file small. | |
| 94 | + | captureBedrockPricingPage(ctx, t, client, sigv4, region, outDir) | |
| 95 | + | ||
| 96 | + | t.Logf("done. Inspect the files in %s and share them (they contain only public pricing/model data).", outDir) | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | // captureBedrockPricingPage issues a single GetProducts page filtered to | |
| 100 | + | // the region and the Anthropic provider (the family we care about most) | |
| 101 | + | // and writes the raw response. Anthropic is the case the aws-cli #9567 | |
| 102 | + | // report flagged as possibly missing output-token rows, so capturing it | |
| 103 | + | // specifically lets us confirm or refute that. | |
| 104 | + | func captureBedrockPricingPage(ctx context.Context, t *testing.T, client *http.Client, creds *bedrockSigV4Creds, region, outDir string) { | |
| 105 | + | const pricingRegion = "us-east-1" | |
| 106 | + | url := "https://api.pricing." + pricingRegion + ".amazonaws.com/" | |
| 107 | + | ||
| 108 | + | reqBody := map[string]any{ | |
| 109 | + | "ServiceCode": "AmazonBedrock", | |
| 110 | + | "FormatVersion": "aws_v1", | |
| 111 | + | "MaxResults": 100, | |
| 112 | + | "Filters": []map[string]string{ | |
| 113 | + | {"Type": "TERM_MATCH", "Field": "regionCode", "Value": region}, | |
| 114 | + | {"Type": "TERM_MATCH", "Field": "provider", "Value": "Anthropic"}, | |
| 115 | + | }, | |
| 116 | + | } | |
| 117 | + | payload, err := json.Marshal(reqBody) | |
| 118 | + | if err != nil { | |
| 119 | + | t.Errorf("marshal pricing request: %v", err) | |
| 120 | + | return | |
| 121 | + | } | |
| 122 | + | req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytesReader(payload)) | |
| 123 | + | if err != nil { | |
| 124 | + | t.Errorf("build pricing request: %v", err) | |
| 125 | + | return | |
| 126 | + | } | |
| 127 | + | req.Header.Set("content-type", "application/x-amz-json-1.1") | |
| 128 | + | req.Header.Set("x-amz-target", "AWSPriceListService.GetProducts") | |
| 129 | + | if err := signSigV4(req, payload, "pricing", pricingRegion, creds, time.Now().UTC()); err != nil { | |
| 130 | + | t.Errorf("sign pricing request: %v", err) | |
| 131 | + | return | |
| 132 | + | } | |
| 133 | + | resp, err := client.Do(req) | |
| 134 | + | if err != nil { | |
| 135 | + | t.Errorf("pricing request failed: %v", err) | |
| 136 | + | return | |
| 137 | + | } | |
| 138 | + | defer resp.Body.Close() | |
| 139 | + | var page struct { | |
| 140 | + | PriceList []string `json:"PriceList"` | |
| 141 | + | NextToken string `json:"NextToken"` | |
| 142 | + | } | |
| 143 | + | body := readAll(t, resp) | |
| 144 | + | if resp.StatusCode < 200 || resp.StatusCode >= 300 { | |
| 145 | + | t.Errorf("pricing http %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) | |
| 146 | + | return | |
| 147 | + | } | |
| 148 | + | writeCapture(t, outDir, "pricing-anthropic-raw.json", body) | |
| 149 | + | ||
| 150 | + | // Pretty-print each PriceList entry (they are JSON-encoded strings) | |
| 151 | + | // into a single readable file, and log a compact attribute summary. | |
| 152 | + | if err := json.Unmarshal(body, &page); err != nil { | |
| 153 | + | t.Errorf("parse pricing page: %v", err) | |
| 154 | + | return | |
| 155 | + | } | |
| 156 | + | t.Logf("pricing page returned %d PriceList entries (NextToken present: %v)", len(page.PriceList), page.NextToken != "") | |
| 157 | + | var pretty strings.Builder | |
| 158 | + | for _, raw := range page.PriceList { | |
| 159 | + | var obj any | |
| 160 | + | if err := json.Unmarshal([]byte(raw), &obj); err != nil { | |
| 161 | + | continue | |
| 162 | + | } | |
| 163 | + | b, _ := json.MarshalIndent(obj, "", " ") | |
| 164 | + | pretty.Write(b) | |
| 165 | + | pretty.WriteByte('\n') | |
| 166 | + | logBedrockPriceAttributes(t, raw) | |
| 167 | + | } | |
| 168 | + | writeCapture(t, outDir, "pricing-anthropic-pretty.json", []byte(pretty.String())) | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | func logBedrockModelIDs(t *testing.T, body []byte) { | |
| 172 | + | var fm struct { | |
| 173 | + | ModelSummaries []struct { | |
| 174 | + | ModelID string `json:"modelId"` | |
| 175 | + | OutputModalities []string `json:"outputModalities"` | |
| 176 | + | } `json:"modelSummaries"` | |
| 177 | + | } | |
| 178 | + | if err := json.Unmarshal(body, &fm); err != nil { | |
| 179 | + | return | |
| 180 | + | } | |
| 181 | + | t.Logf("ListFoundationModels: %d models", len(fm.ModelSummaries)) | |
| 182 | + | for _, s := range fm.ModelSummaries { | |
| 183 | + | if strings.Contains(strings.ToLower(s.ModelID), "anthropic") { | |
| 184 | + | t.Logf(" modelId=%s outputModalities=%v", s.ModelID, s.OutputModalities) | |
| 185 | + | } | |
| 186 | + | } | |
| 187 | + | } | |
| 188 | + | ||
| 189 | + | func logBedrockPriceAttributes(t *testing.T, raw string) { | |
| 190 | + | var entry struct { | |
| 191 | + | Product struct { | |
| 192 | + | Attributes map[string]string `json:"attributes"` | |
| 193 | + | } `json:"product"` | |
| 194 | + | } | |
| 195 | + | if err := json.Unmarshal([]byte(raw), &entry); err != nil { | |
| 196 | + | return | |
| 197 | + | } | |
| 198 | + | a := entry.Product.Attributes | |
| 199 | + | t.Logf(" price: model=%q inferenceType=%q feature=%q usagetype=%q", | |
| 200 | + | a["model"], a["inferenceType"], a["feature"], a["usagetype"]) | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | func writeCapture(t *testing.T, dir, name string, body []byte) { | |
| 204 | + | t.Helper() | |
| 205 | + | path := dir + string(os.PathSeparator) + name | |
| 206 | + | if err := os.WriteFile(path, body, 0o644); err != nil { | |
| 207 | + | t.Errorf("write %s: %v", path, err) | |
| 208 | + | return | |
| 209 | + | } | |
| 210 | + | t.Logf("wrote %s (%d bytes)", path, len(body)) | |
| 211 | + | } | |
| 212 | + | ||
| 213 | + | func readAll(t *testing.T, resp *http.Response) []byte { | |
| 214 | + | t.Helper() | |
| 215 | + | buf := make([]byte, 0, 1<<20) | |
| 216 | + | tmp := make([]byte, 32<<10) | |
| 217 | + | for { | |
| 218 | + | n, err := resp.Body.Read(tmp) | |
| 219 | + | if n > 0 { | |
| 220 | + | buf = append(buf, tmp[:n]...) | |
| 221 | + | } | |
| 222 | + | if err != nil { | |
| 223 | + | break | |
| 224 | + | } | |
| 225 | + | if len(buf) > 32<<20 { | |
| 226 | + | break | |
| 227 | + | } | |
| 228 | + | } | |
| 229 | + | return buf | |
| 230 | + | } | |
| @@ -0,0 +1,230 @@ | |||||
| 1 | + | package provider | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "encoding/json" | |||
| 6 | + | "net/http" | |||
| 7 | + | "os" | |||
| 8 | + | "strings" | |||
| 9 | + | "testing" | |||
| 10 | + | "time" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // Live Bedrock discovery diagnostic / capture helper. | |||
| 14 | + | // | |||
| 15 | + | // This is NOT a validation test. It lets a developer with real AWS | |||
| 16 | + | // credentials capture actual control-plane and Price List responses to | |||
| 17 | + | // verify discovery output and re-check the pricing situation. It never | |||
| 18 | + | // runs in `go test ./...` unless explicitly enabled, so CI and unpaid | |||
| 19 | + | // checkouts never hit the network. | |||
| 20 | + | // | |||
| 21 | + | // Background: a live capture (Sep 2026, us-east-1) confirmed the AWS | |||
| 22 | + | // Price List API returns only stale, input-only rows for legacy Anthropic | |||
| 23 | + | // models (Claude 2.x / 3 Sonnet / 3 Haiku) and no output/cache prices for | |||
| 24 | + | // any current model, so zot does NOT enrich pricing from it — the catalog | |||
| 25 | + | // stays authoritative. This helper is retained to detect if/when AWS | |||
| 26 | + | // fixes that, and to confirm ListFoundationModels / ListInferenceProfiles | |||
| 27 | + | // shapes when they change. | |||
| 28 | + | // | |||
| 29 | + | // Enable and run: | |||
| 30 | + | // | |||
| 31 | + | // ZOT_BEDROCK_CAPTURE=1 \ | |||
| 32 | + | // AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... [AWS_SESSION_TOKEN=...] \ | |||
| 33 | + | // AWS_REGION=us-east-1 \ | |||
| 34 | + | // go test ./packages/provider -run TestBedrockCaptureLive -v | |||
| 35 | + | // | |||
| 36 | + | // Or with a profile: | |||
| 37 | + | // | |||
| 38 | + | // ZOT_BEDROCK_CAPTURE=1 AWS_PROFILE=my-sso AWS_REGION=us-east-1 \ | |||
| 39 | + | // go test ./packages/provider -run TestBedrockCaptureLive -v | |||
| 40 | + | // | |||
| 41 | + | // Output is written to files under $ZOT_BEDROCK_CAPTURE_DIR (default: a | |||
| 42 | + | // temp dir printed in the log). Secrets are never captured — only the | |||
| 43 | + | // public model/pricing JSON. | |||
| 44 | + | // | |||
| 45 | + | // Requires the caller's IAM principal to allow: | |||
| 46 | + | // - bedrock:ListFoundationModels | |||
| 47 | + | // - bedrock:ListInferenceProfiles | |||
| 48 | + | // - pricing:GetProducts | |||
| 49 | + | func TestBedrockCaptureLive(t *testing.T) { | |||
| 50 | + | if strings.TrimSpace(os.Getenv("ZOT_BEDROCK_CAPTURE")) == "" { | |||
| 51 | + | t.Skip("set ZOT_BEDROCK_CAPTURE=1 (with AWS creds) to capture live Bedrock responses") | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | _, sigv4 := resolveBedrockAuth("") | |||
| 55 | + | if sigv4 == nil { | |||
| 56 | + | t.Fatal("no SigV4 credentials found; set AWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEY or AWS_PROFILE") | |||
| 57 | + | } | |||
| 58 | + | region := bedrockResolveRegion() | |||
| 59 | + | t.Logf("capturing Bedrock responses for region=%s", region) | |||
| 60 | + | ||||
| 61 | + | outDir := strings.TrimSpace(os.Getenv("ZOT_BEDROCK_CAPTURE_DIR")) | |||
| 62 | + | if outDir == "" { | |||
| 63 | + | outDir = t.TempDir() | |||
| 64 | + | } | |||
| 65 | + | t.Logf("writing captures to %s", outDir) | |||
| 66 | + | ||||
| 67 | + | client := &http.Client{Timeout: 30 * time.Second} | |||
| 68 | + | ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) | |||
| 69 | + | defer cancel() | |||
| 70 | + | ||||
| 71 | + | // 1. ListFoundationModels (control plane, GET). | |||
| 72 | + | fmURL := "https://bedrock." + region + ".amazonaws.com/foundation-models" | |||
| 73 | + | if body, err := bedrockSignedControlPlane(ctx, client, sigv4, region, http.MethodGet, fmURL, nil); err != nil { | |||
| 74 | + | t.Errorf("ListFoundationModels failed: %v", err) | |||
| 75 | + | } else { | |||
| 76 | + | writeCapture(t, outDir, "foundation-models.json", body) | |||
| 77 | + | // Also log the summarized model IDs so the capturer can eyeball them. | |||
| 78 | + | logBedrockModelIDs(t, body) | |||
| 79 | + | } | |||
| 80 | + | ||||
| 81 | + | // 2. ListInferenceProfiles (control plane, GET, paginated). | |||
| 82 | + | ipURL := "https://bedrock." + region + ".amazonaws.com/inference-profiles?maxResults=1000" | |||
| 83 | + | if body, err := bedrockSignedControlPlane(ctx, client, sigv4, region, http.MethodGet, ipURL, nil); err != nil { | |||
| 84 | + | t.Errorf("ListInferenceProfiles failed: %v", err) | |||
| 85 | + | } else { | |||
| 86 | + | writeCapture(t, outDir, "inference-profiles.json", body) | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | // 3. GetProducts for AmazonBedrock (Price List API, POST). | |||
| 90 | + | // Capture the FIRST page raw so we can inspect real attribute | |||
| 91 | + | // names, inferenceType values, feature strings, and units. We | |||
| 92 | + | // deliberately do not paginate here — one page is enough to fix | |||
| 93 | + | // the fixtures, and it keeps the captured file small. | |||
| 94 | + | captureBedrockPricingPage(ctx, t, client, sigv4, region, outDir) | |||
| 95 | + | ||||
| 96 | + | t.Logf("done. Inspect the files in %s and share them (they contain only public pricing/model data).", outDir) | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | // captureBedrockPricingPage issues a single GetProducts page filtered to | |||
| 100 | + | // the region and the Anthropic provider (the family we care about most) | |||
| 101 | + | // and writes the raw response. Anthropic is the case the aws-cli #9567 | |||
| 102 | + | // report flagged as possibly missing output-token rows, so capturing it | |||
| 103 | + | // specifically lets us confirm or refute that. | |||
| 104 | + | func captureBedrockPricingPage(ctx context.Context, t *testing.T, client *http.Client, creds *bedrockSigV4Creds, region, outDir string) { | |||
| 105 | + | const pricingRegion = "us-east-1" | |||
| 106 | + | url := "https://api.pricing." + pricingRegion + ".amazonaws.com/" | |||
| 107 | + | ||||
| 108 | + | reqBody := map[string]any{ | |||
| 109 | + | "ServiceCode": "AmazonBedrock", | |||
| 110 | + | "FormatVersion": "aws_v1", | |||
| 111 | + | "MaxResults": 100, | |||
| 112 | + | "Filters": []map[string]string{ | |||
| 113 | + | {"Type": "TERM_MATCH", "Field": "regionCode", "Value": region}, | |||
| 114 | + | {"Type": "TERM_MATCH", "Field": "provider", "Value": "Anthropic"}, | |||
| 115 | + | }, | |||
| 116 | + | } | |||
| 117 | + | payload, err := json.Marshal(reqBody) | |||
| 118 | + | if err != nil { | |||
| 119 | + | t.Errorf("marshal pricing request: %v", err) | |||
| 120 | + | return | |||
| 121 | + | } | |||
| 122 | + | req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytesReader(payload)) | |||
| 123 | + | if err != nil { | |||
| 124 | + | t.Errorf("build pricing request: %v", err) | |||
| 125 | + | return | |||
| 126 | + | } | |||
| 127 | + | req.Header.Set("content-type", "application/x-amz-json-1.1") | |||
| 128 | + | req.Header.Set("x-amz-target", "AWSPriceListService.GetProducts") | |||
| 129 | + | if err := signSigV4(req, payload, "pricing", pricingRegion, creds, time.Now().UTC()); err != nil { | |||
| 130 | + | t.Errorf("sign pricing request: %v", err) | |||
| 131 | + | return | |||
| 132 | + | } | |||
| 133 | + | resp, err := client.Do(req) | |||
| 134 | + | if err != nil { | |||
| 135 | + | t.Errorf("pricing request failed: %v", err) | |||
| 136 | + | return | |||
| 137 | + | } | |||
| 138 | + | defer resp.Body.Close() | |||
| 139 | + | var page struct { | |||
| 140 | + | PriceList []string `json:"PriceList"` | |||
| 141 | + | NextToken string `json:"NextToken"` | |||
| 142 | + | } | |||
| 143 | + | body := readAll(t, resp) | |||
| 144 | + | if resp.StatusCode < 200 || resp.StatusCode >= 300 { | |||
| 145 | + | t.Errorf("pricing http %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) | |||
| 146 | + | return | |||
| 147 | + | } | |||
| 148 | + | writeCapture(t, outDir, "pricing-anthropic-raw.json", body) | |||
| 149 | + | ||||
| 150 | + | // Pretty-print each PriceList entry (they are JSON-encoded strings) | |||
| 151 | + | // into a single readable file, and log a compact attribute summary. | |||
| 152 | + | if err := json.Unmarshal(body, &page); err != nil { | |||
| 153 | + | t.Errorf("parse pricing page: %v", err) | |||
| 154 | + | return | |||
| 155 | + | } | |||
| 156 | + | t.Logf("pricing page returned %d PriceList entries (NextToken present: %v)", len(page.PriceList), page.NextToken != "") | |||
| 157 | + | var pretty strings.Builder | |||
| 158 | + | for _, raw := range page.PriceList { | |||
| 159 | + | var obj any | |||
| 160 | + | if err := json.Unmarshal([]byte(raw), &obj); err != nil { | |||
| 161 | + | continue | |||
| 162 | + | } | |||
| 163 | + | b, _ := json.MarshalIndent(obj, "", " ") | |||
| 164 | + | pretty.Write(b) | |||
| 165 | + | pretty.WriteByte('\n') | |||
| 166 | + | logBedrockPriceAttributes(t, raw) | |||
| 167 | + | } | |||
| 168 | + | writeCapture(t, outDir, "pricing-anthropic-pretty.json", []byte(pretty.String())) | |||
| 169 | + | } | |||
| 170 | + | ||||
| 171 | + | func logBedrockModelIDs(t *testing.T, body []byte) { | |||
| 172 | + | var fm struct { | |||
| 173 | + | ModelSummaries []struct { | |||
| 174 | + | ModelID string `json:"modelId"` | |||
| 175 | + | OutputModalities []string `json:"outputModalities"` | |||
| 176 | + | } `json:"modelSummaries"` | |||
| 177 | + | } | |||
| 178 | + | if err := json.Unmarshal(body, &fm); err != nil { | |||
| 179 | + | return | |||
| 180 | + | } | |||
| 181 | + | t.Logf("ListFoundationModels: %d models", len(fm.ModelSummaries)) | |||
| 182 | + | for _, s := range fm.ModelSummaries { | |||
| 183 | + | if strings.Contains(strings.ToLower(s.ModelID), "anthropic") { | |||
| 184 | + | t.Logf(" modelId=%s outputModalities=%v", s.ModelID, s.OutputModalities) | |||
| 185 | + | } | |||
| 186 | + | } | |||
| 187 | + | } | |||
| 188 | + | ||||
| 189 | + | func logBedrockPriceAttributes(t *testing.T, raw string) { | |||
| 190 | + | var entry struct { | |||
| 191 | + | Product struct { | |||
| 192 | + | Attributes map[string]string `json:"attributes"` | |||
| 193 | + | } `json:"product"` | |||
| 194 | + | } | |||
| 195 | + | if err := json.Unmarshal([]byte(raw), &entry); err != nil { | |||
| 196 | + | return | |||
| 197 | + | } | |||
| 198 | + | a := entry.Product.Attributes | |||
| 199 | + | t.Logf(" price: model=%q inferenceType=%q feature=%q usagetype=%q", | |||
| 200 | + | a["model"], a["inferenceType"], a["feature"], a["usagetype"]) | |||
| 201 | + | } | |||
| 202 | + | ||||
| 203 | + | func writeCapture(t *testing.T, dir, name string, body []byte) { | |||
| 204 | + | t.Helper() | |||
| 205 | + | path := dir + string(os.PathSeparator) + name | |||
| 206 | + | if err := os.WriteFile(path, body, 0o644); err != nil { | |||
| 207 | + | t.Errorf("write %s: %v", path, err) | |||
| 208 | + | return | |||
| 209 | + | } | |||
| 210 | + | t.Logf("wrote %s (%d bytes)", path, len(body)) | |||
| 211 | + | } | |||
| 212 | + | ||||
| 213 | + | func readAll(t *testing.T, resp *http.Response) []byte { | |||
| 214 | + | t.Helper() | |||
| 215 | + | buf := make([]byte, 0, 1<<20) | |||
| 216 | + | tmp := make([]byte, 32<<10) | |||
| 217 | + | for { | |||
| 218 | + | n, err := resp.Body.Read(tmp) | |||
| 219 | + | if n > 0 { | |||
| 220 | + | buf = append(buf, tmp[:n]...) | |||
| 221 | + | } | |||
| 222 | + | if err != nil { | |||
| 223 | + | break | |||
| 224 | + | } | |||
| 225 | + | if len(buf) > 32<<20 { | |||
| 226 | + | break | |||
| 227 | + | } | |||
| 228 | + | } | |||
| 229 | + | return buf | |||
| 230 | + | } | |||
A packages/provider/discover_bedrock_test.go +171 -0
| @@ -0,0 +1,171 @@ | |||
| 1 | + | package provider | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "io" | |
| 6 | + | "net/http" | |
| 7 | + | "net/http/httptest" | |
| 8 | + | "strings" | |
| 9 | + | "testing" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | type bedrockDiscoveryTestTransport func(*http.Request) (*http.Response, error) | |
| 13 | + | ||
| 14 | + | func (f bedrockDiscoveryTestTransport) RoundTrip(r *http.Request) (*http.Response, error) { | |
| 15 | + | return f(r) | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | func TestDiscoverBedrockUsesSigV4WithBearer(t *testing.T) { | |
| 19 | + | t.Setenv("AWS_BEARER_TOKEN_BEDROCK", "bearer") | |
| 20 | + | t.Setenv("AWS_ACCESS_KEY_ID", "AKID") | |
| 21 | + | t.Setenv("AWS_SECRET_ACCESS_KEY", "SECRET") | |
| 22 | + | original := http.DefaultTransport | |
| 23 | + | t.Cleanup(func() { http.DefaultTransport = original }) | |
| 24 | + | http.DefaultTransport = bedrockDiscoveryTestTransport(func(r *http.Request) (*http.Response, error) { | |
| 25 | + | if !strings.Contains(r.Header.Get("Authorization"), "Credential=AKID/") { | |
| 26 | + | t.Errorf("discovery request was not signed with SigV4") | |
| 27 | + | } | |
| 28 | + | body := `{"modelSummaries":[{"modelId":"test.new-model","outputModalities":["TEXT"]}]}` | |
| 29 | + | if r.URL.Path == "/inference-profiles" { | |
| 30 | + | body = `{"inferenceProfileSummaries":[]}` | |
| 31 | + | } | |
| 32 | + | return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)}, nil | |
| 33 | + | }) | |
| 34 | + | models, err := DiscoverBedrock(context.Background(), "us-east-1") | |
| 35 | + | if err != nil || len(models) != 1 || models[0].ID != "test.new-model" { | |
| 36 | + | t.Fatalf("discovered models = %v, err = %v", models, err) | |
| 37 | + | } | |
| 38 | + | if bearer, sigv4 := resolveBedrockAuth("bearer"); bearer != "bearer" || sigv4 != nil { | |
| 39 | + | t.Fatal("inference must continue to prefer the bearer token") | |
| 40 | + | } | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | func TestBedrockEmitsText(t *testing.T) { | |
| 44 | + | if !bedrockEmitsText(nil) { | |
| 45 | + | t.Error("nil modalities should default to text") | |
| 46 | + | } | |
| 47 | + | if !bedrockEmitsText([]string{"TEXT"}) { | |
| 48 | + | t.Error("TEXT should emit text") | |
| 49 | + | } | |
| 50 | + | if bedrockEmitsText([]string{"IMAGE"}) { | |
| 51 | + | t.Error("IMAGE-only should not emit text") | |
| 52 | + | } | |
| 53 | + | if !bedrockEmitsText([]string{"IMAGE", "TEXT"}) { | |
| 54 | + | t.Error("mixed with TEXT should emit text") | |
| 55 | + | } | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | func TestResolveBedrockAuthBearerVsSigV4(t *testing.T) { | |
| 59 | + | t.Setenv("AWS_BEARER_TOKEN_BEDROCK", "") | |
| 60 | + | t.Setenv("AWS_ACCESS_KEY_ID", "") | |
| 61 | + | t.Setenv("AWS_SECRET_ACCESS_KEY", "") | |
| 62 | + | t.Setenv("AWS_SESSION_TOKEN", "") | |
| 63 | + | t.Setenv("AWS_PROFILE", "") | |
| 64 | + | ||
| 65 | + | // Explicit bearer via apiKey. | |
| 66 | + | if b, s := resolveBedrockAuth("tok-123"); b != "tok-123" || s != nil { | |
| 67 | + | t.Errorf("explicit bearer: got (%q,%v)", b, s) | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | // Sentinel <aws> is not a bearer; falls through to SigV4 env. | |
| 71 | + | t.Setenv("AWS_ACCESS_KEY_ID", "AKID") | |
| 72 | + | t.Setenv("AWS_SECRET_ACCESS_KEY", "SECRET") | |
| 73 | + | b, s := resolveBedrockAuth("<aws>") | |
| 74 | + | if b != "" || s == nil { | |
| 75 | + | t.Fatalf("sentinel should resolve SigV4, got (%q,%v)", b, s) | |
| 76 | + | } | |
| 77 | + | if s.accessKeyID != "AKID" || s.secretAccessKey != "SECRET" { | |
| 78 | + | t.Errorf("sigv4 creds = %+v", s) | |
| 79 | + | } | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | // TestBedrockListModelIDs parses control-plane responses shaped exactly | |
| 83 | + | // like the real ones captured from a live account (see the doc note in | |
| 84 | + | // discover_bedrock.go): ListFoundationModels returns modelSummaries with | |
| 85 | + | // modelId + outputModalities; ListInferenceProfiles returns | |
| 86 | + | // inferenceProfileSummaries with inferenceProfileId and paginates via | |
| 87 | + | // nextToken. Embedding/image-only models are filtered out; profile IDs | |
| 88 | + | // are included; duplicates are de-duplicated. | |
| 89 | + | func TestBedrockListModelIDs(t *testing.T) { | |
| 90 | + | page1 := `{"inferenceProfileSummaries":[{"inferenceProfileId":"us.anthropic.claude-opus-5-5"}],"nextToken":"N+E/T= &"}` | |
| 91 | + | page2 := `{"inferenceProfileSummaries":[{"inferenceProfileId":"eu.anthropic.claude-sonnet-5"}]}` | |
| 92 | + | foundation := `{"modelSummaries":[ | |
| 93 | + | {"modelId":"anthropic.claude-opus-5-5","outputModalities":["TEXT"]}, | |
| 94 | + | {"modelId":"amazon.titan-embed-text-v2","outputModalities":["EMBEDDING"]}, | |
| 95 | + | {"modelId":"stability.sd3-large","outputModalities":["IMAGE"]}, | |
| 96 | + | {"modelId":"anthropic.claude-opus-5-5","outputModalities":["TEXT"]} | |
| 97 | + | ]}` | |
| 98 | + | ||
| 99 | + | var ipHits int | |
| 100 | + | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 101 | + | w.Header().Set("content-type", "application/json") | |
| 102 | + | switch { | |
| 103 | + | case strings.Contains(r.URL.Path, "/foundation-models"): | |
| 104 | + | w.Write([]byte(foundation)) | |
| 105 | + | case strings.Contains(r.URL.Path, "/inference-profiles"): | |
| 106 | + | ipHits++ | |
| 107 | + | switch token := r.URL.Query().Get("nextToken"); token { | |
| 108 | + | case "": | |
| 109 | + | w.Write([]byte(page1)) | |
| 110 | + | case "N+E/T= &": | |
| 111 | + | w.Write([]byte(page2)) | |
| 112 | + | default: | |
| 113 | + | t.Errorf("unexpected pagination token %q", token) | |
| 114 | + | w.WriteHeader(http.StatusBadRequest) | |
| 115 | + | } | |
| 116 | + | default: | |
| 117 | + | w.WriteHeader(http.StatusNotFound) | |
| 118 | + | } | |
| 119 | + | })) | |
| 120 | + | defer srv.Close() | |
| 121 | + | ||
| 122 | + | creds := &bedrockSigV4Creds{accessKeyID: "AKID", secretAccessKey: "SECRET"} | |
| 123 | + | ids, err := bedrockListModelIDs(context.Background(), srv.Client(), creds, "us-east-1", srv.URL) | |
| 124 | + | if err != nil { | |
| 125 | + | t.Fatalf("bedrockListModelIDs: %v", err) | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | want := []string{ | |
| 129 | + | "anthropic.claude-opus-5-5", // text foundation model, de-duplicated | |
| 130 | + | "us.anthropic.claude-opus-5-5", // profile page 1 | |
| 131 | + | "eu.anthropic.claude-sonnet-5", // profile page 2 (pagination followed) | |
| 132 | + | } | |
| 133 | + | got := map[string]int{} | |
| 134 | + | for _, id := range ids { | |
| 135 | + | got[id]++ | |
| 136 | + | } | |
| 137 | + | for _, w := range want { | |
| 138 | + | if got[w] != 1 { | |
| 139 | + | t.Errorf("expected exactly one %q, got %d (all=%v)", w, got[w], ids) | |
| 140 | + | } | |
| 141 | + | } | |
| 142 | + | // Embedding and image-only models must be filtered out. | |
| 143 | + | for _, bad := range []string{"amazon.titan-embed-text-v2", "stability.sd3-large"} { | |
| 144 | + | if got[bad] != 0 { | |
| 145 | + | t.Errorf("non-text model %q should be filtered out", bad) | |
| 146 | + | } | |
| 147 | + | } | |
| 148 | + | if ipHits != 2 { | |
| 149 | + | t.Errorf("expected 2 inference-profile page requests (pagination), got %d", ipHits) | |
| 150 | + | } | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | // TestBedrockControlPlaneHTTPError confirms non-2xx responses surface as | |
| 154 | + | // errors with the status and body, so discovery fails loudly (and the | |
| 155 | + | // caller then falls back to the catalog) rather than silently parsing junk. | |
| 156 | + | func TestBedrockControlPlaneHTTPError(t *testing.T) { | |
| 157 | + | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 158 | + | w.WriteHeader(http.StatusForbidden) | |
| 159 | + | w.Write([]byte(`{"message":"not authorized"}`)) | |
| 160 | + | })) | |
| 161 | + | defer srv.Close() | |
| 162 | + | ||
| 163 | + | creds := &bedrockSigV4Creds{accessKeyID: "AKID", secretAccessKey: "SECRET"} | |
| 164 | + | _, err := bedrockSignedControlPlane(context.Background(), srv.Client(), creds, "us-east-1", http.MethodGet, srv.URL+"/foundation-models", nil) | |
| 165 | + | if err == nil { | |
| 166 | + | t.Fatal("expected error on 403") | |
| 167 | + | } | |
| 168 | + | if !strings.Contains(err.Error(), "403") || !strings.Contains(err.Error(), "not authorized") { | |
| 169 | + | t.Errorf("error should carry status and body: %v", err) | |
| 170 | + | } | |
| 171 | + | } | |
| @@ -0,0 +1,171 @@ | |||||
| 1 | + | package provider | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "io" | |||
| 6 | + | "net/http" | |||
| 7 | + | "net/http/httptest" | |||
| 8 | + | "strings" | |||
| 9 | + | "testing" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | type bedrockDiscoveryTestTransport func(*http.Request) (*http.Response, error) | |||
| 13 | + | ||||
| 14 | + | func (f bedrockDiscoveryTestTransport) RoundTrip(r *http.Request) (*http.Response, error) { | |||
| 15 | + | return f(r) | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | func TestDiscoverBedrockUsesSigV4WithBearer(t *testing.T) { | |||
| 19 | + | t.Setenv("AWS_BEARER_TOKEN_BEDROCK", "bearer") | |||
| 20 | + | t.Setenv("AWS_ACCESS_KEY_ID", "AKID") | |||
| 21 | + | t.Setenv("AWS_SECRET_ACCESS_KEY", "SECRET") | |||
| 22 | + | original := http.DefaultTransport | |||
| 23 | + | t.Cleanup(func() { http.DefaultTransport = original }) | |||
| 24 | + | http.DefaultTransport = bedrockDiscoveryTestTransport(func(r *http.Request) (*http.Response, error) { | |||
| 25 | + | if !strings.Contains(r.Header.Get("Authorization"), "Credential=AKID/") { | |||
| 26 | + | t.Errorf("discovery request was not signed with SigV4") | |||
| 27 | + | } | |||
| 28 | + | body := `{"modelSummaries":[{"modelId":"test.new-model","outputModalities":["TEXT"]}]}` | |||
| 29 | + | if r.URL.Path == "/inference-profiles" { | |||
| 30 | + | body = `{"inferenceProfileSummaries":[]}` | |||
| 31 | + | } | |||
| 32 | + | return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)}, nil | |||
| 33 | + | }) | |||
| 34 | + | models, err := DiscoverBedrock(context.Background(), "us-east-1") | |||
| 35 | + | if err != nil || len(models) != 1 || models[0].ID != "test.new-model" { | |||
| 36 | + | t.Fatalf("discovered models = %v, err = %v", models, err) | |||
| 37 | + | } | |||
| 38 | + | if bearer, sigv4 := resolveBedrockAuth("bearer"); bearer != "bearer" || sigv4 != nil { | |||
| 39 | + | t.Fatal("inference must continue to prefer the bearer token") | |||
| 40 | + | } | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | func TestBedrockEmitsText(t *testing.T) { | |||
| 44 | + | if !bedrockEmitsText(nil) { | |||
| 45 | + | t.Error("nil modalities should default to text") | |||
| 46 | + | } | |||
| 47 | + | if !bedrockEmitsText([]string{"TEXT"}) { | |||
| 48 | + | t.Error("TEXT should emit text") | |||
| 49 | + | } | |||
| 50 | + | if bedrockEmitsText([]string{"IMAGE"}) { | |||
| 51 | + | t.Error("IMAGE-only should not emit text") | |||
| 52 | + | } | |||
| 53 | + | if !bedrockEmitsText([]string{"IMAGE", "TEXT"}) { | |||
| 54 | + | t.Error("mixed with TEXT should emit text") | |||
| 55 | + | } | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | func TestResolveBedrockAuthBearerVsSigV4(t *testing.T) { | |||
| 59 | + | t.Setenv("AWS_BEARER_TOKEN_BEDROCK", "") | |||
| 60 | + | t.Setenv("AWS_ACCESS_KEY_ID", "") | |||
| 61 | + | t.Setenv("AWS_SECRET_ACCESS_KEY", "") | |||
| 62 | + | t.Setenv("AWS_SESSION_TOKEN", "") | |||
| 63 | + | t.Setenv("AWS_PROFILE", "") | |||
| 64 | + | ||||
| 65 | + | // Explicit bearer via apiKey. | |||
| 66 | + | if b, s := resolveBedrockAuth("tok-123"); b != "tok-123" || s != nil { | |||
| 67 | + | t.Errorf("explicit bearer: got (%q,%v)", b, s) | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | // Sentinel <aws> is not a bearer; falls through to SigV4 env. | |||
| 71 | + | t.Setenv("AWS_ACCESS_KEY_ID", "AKID") | |||
| 72 | + | t.Setenv("AWS_SECRET_ACCESS_KEY", "SECRET") | |||
| 73 | + | b, s := resolveBedrockAuth("<aws>") | |||
| 74 | + | if b != "" || s == nil { | |||
| 75 | + | t.Fatalf("sentinel should resolve SigV4, got (%q,%v)", b, s) | |||
| 76 | + | } | |||
| 77 | + | if s.accessKeyID != "AKID" || s.secretAccessKey != "SECRET" { | |||
| 78 | + | t.Errorf("sigv4 creds = %+v", s) | |||
| 79 | + | } | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | // TestBedrockListModelIDs parses control-plane responses shaped exactly | |||
| 83 | + | // like the real ones captured from a live account (see the doc note in | |||
| 84 | + | // discover_bedrock.go): ListFoundationModels returns modelSummaries with | |||
| 85 | + | // modelId + outputModalities; ListInferenceProfiles returns | |||
| 86 | + | // inferenceProfileSummaries with inferenceProfileId and paginates via | |||
| 87 | + | // nextToken. Embedding/image-only models are filtered out; profile IDs | |||
| 88 | + | // are included; duplicates are de-duplicated. | |||
| 89 | + | func TestBedrockListModelIDs(t *testing.T) { | |||
| 90 | + | page1 := `{"inferenceProfileSummaries":[{"inferenceProfileId":"us.anthropic.claude-opus-5-5"}],"nextToken":"N+E/T= &"}` | |||
| 91 | + | page2 := `{"inferenceProfileSummaries":[{"inferenceProfileId":"eu.anthropic.claude-sonnet-5"}]}` | |||
| 92 | + | foundation := `{"modelSummaries":[ | |||
| 93 | + | {"modelId":"anthropic.claude-opus-5-5","outputModalities":["TEXT"]}, | |||
| 94 | + | {"modelId":"amazon.titan-embed-text-v2","outputModalities":["EMBEDDING"]}, | |||
| 95 | + | {"modelId":"stability.sd3-large","outputModalities":["IMAGE"]}, | |||
| 96 | + | {"modelId":"anthropic.claude-opus-5-5","outputModalities":["TEXT"]} | |||
| 97 | + | ]}` | |||
| 98 | + | ||||
| 99 | + | var ipHits int | |||
| 100 | + | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 101 | + | w.Header().Set("content-type", "application/json") | |||
| 102 | + | switch { | |||
| 103 | + | case strings.Contains(r.URL.Path, "/foundation-models"): | |||
| 104 | + | w.Write([]byte(foundation)) | |||
| 105 | + | case strings.Contains(r.URL.Path, "/inference-profiles"): | |||
| 106 | + | ipHits++ | |||
| 107 | + | switch token := r.URL.Query().Get("nextToken"); token { | |||
| 108 | + | case "": | |||
| 109 | + | w.Write([]byte(page1)) | |||
| 110 | + | case "N+E/T= &": | |||
| 111 | + | w.Write([]byte(page2)) | |||
| 112 | + | default: | |||
| 113 | + | t.Errorf("unexpected pagination token %q", token) | |||
| 114 | + | w.WriteHeader(http.StatusBadRequest) | |||
| 115 | + | } | |||
| 116 | + | default: | |||
| 117 | + | w.WriteHeader(http.StatusNotFound) | |||
| 118 | + | } | |||
| 119 | + | })) | |||
| 120 | + | defer srv.Close() | |||
| 121 | + | ||||
| 122 | + | creds := &bedrockSigV4Creds{accessKeyID: "AKID", secretAccessKey: "SECRET"} | |||
| 123 | + | ids, err := bedrockListModelIDs(context.Background(), srv.Client(), creds, "us-east-1", srv.URL) | |||
| 124 | + | if err != nil { | |||
| 125 | + | t.Fatalf("bedrockListModelIDs: %v", err) | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | want := []string{ | |||
| 129 | + | "anthropic.claude-opus-5-5", // text foundation model, de-duplicated | |||
| 130 | + | "us.anthropic.claude-opus-5-5", // profile page 1 | |||
| 131 | + | "eu.anthropic.claude-sonnet-5", // profile page 2 (pagination followed) | |||
| 132 | + | } | |||
| 133 | + | got := map[string]int{} | |||
| 134 | + | for _, id := range ids { | |||
| 135 | + | got[id]++ | |||
| 136 | + | } | |||
| 137 | + | for _, w := range want { | |||
| 138 | + | if got[w] != 1 { | |||
| 139 | + | t.Errorf("expected exactly one %q, got %d (all=%v)", w, got[w], ids) | |||
| 140 | + | } | |||
| 141 | + | } | |||
| 142 | + | // Embedding and image-only models must be filtered out. | |||
| 143 | + | for _, bad := range []string{"amazon.titan-embed-text-v2", "stability.sd3-large"} { | |||
| 144 | + | if got[bad] != 0 { | |||
| 145 | + | t.Errorf("non-text model %q should be filtered out", bad) | |||
| 146 | + | } | |||
| 147 | + | } | |||
| 148 | + | if ipHits != 2 { | |||
| 149 | + | t.Errorf("expected 2 inference-profile page requests (pagination), got %d", ipHits) | |||
| 150 | + | } | |||
| 151 | + | } | |||
| 152 | + | ||||
| 153 | + | // TestBedrockControlPlaneHTTPError confirms non-2xx responses surface as | |||
| 154 | + | // errors with the status and body, so discovery fails loudly (and the | |||
| 155 | + | // caller then falls back to the catalog) rather than silently parsing junk. | |||
| 156 | + | func TestBedrockControlPlaneHTTPError(t *testing.T) { | |||
| 157 | + | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 158 | + | w.WriteHeader(http.StatusForbidden) | |||
| 159 | + | w.Write([]byte(`{"message":"not authorized"}`)) | |||
| 160 | + | })) | |||
| 161 | + | defer srv.Close() | |||
| 162 | + | ||||
| 163 | + | creds := &bedrockSigV4Creds{accessKeyID: "AKID", secretAccessKey: "SECRET"} | |||
| 164 | + | _, err := bedrockSignedControlPlane(context.Background(), srv.Client(), creds, "us-east-1", http.MethodGet, srv.URL+"/foundation-models", nil) | |||
| 165 | + | if err == nil { | |||
| 166 | + | t.Fatal("expected error on 403") | |||
| 167 | + | } | |||
| 168 | + | if !strings.Contains(err.Error(), "403") || !strings.Contains(err.Error(), "not authorized") { | |||
| 169 | + | t.Errorf("error should carry status and body: %v", err) | |||
| 170 | + | } | |||
| 171 | + | } | |||