@clawdbuilder/jig-bedrock 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -15
- package/package.json +1 -1
- package/src/aws-discovery.ts +187 -75
- package/src/catalog.ts +108 -13
- package/src/commands.ts +7 -3
- package/src/config.ts +43 -14
- package/src/provider.ts +14 -11
package/README.md
CHANGED
|
@@ -24,24 +24,33 @@ calling AWS.
|
|
|
24
24
|
|
|
25
25
|
## Minimum settings
|
|
26
26
|
|
|
27
|
-
With Jig, add a `bedrock` section to `jig.json` (or use the environment):
|
|
27
|
+
With Jig, add a `bedrock` section to `jig.json` (or use the environment). Nothing is required: with no settings, the
|
|
28
|
+
provider lists AWS-defined cross-region Claude profiles in `regions`.
|
|
28
29
|
|
|
29
30
|
```json
|
|
30
|
-
{ "bedrock": { "
|
|
31
|
+
{ "bedrock": { "regions": ["us-east-1"] } }
|
|
31
32
|
```
|
|
32
33
|
|
|
33
34
|
A plain Pi install has no `jig.json`: put the same keys in `~/.pi/agent/bedrock.json` (or in a `bedrock` object in that
|
|
34
35
|
file). With Jig, that file is read first and Jig's `bedrock` section is merged over it. A project's `jig.json` may set the
|
|
35
36
|
catalog keys, but `ssoLoginCommand`, `usageDir` and `browser` are read only from the user's file or the environment.
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
Three modes, chosen by the settings:
|
|
39
|
+
|
|
40
|
+
1. **System-defined (default).** No tag settings: lists AWS-defined cross-region Claude inference profiles with
|
|
41
|
+
`ListInferenceProfiles --type-equals SYSTEM_DEFINED`.
|
|
42
|
+
2. **Tag filter.** `tagKey` and `tagValue` filter application profiles by any tag. `team` is a shorthand for
|
|
43
|
+
`tagValue` with `tagKey` defaulting to `Team`. `activeTagKey`/`activeTagValue` are optional; when `team` is set and
|
|
44
|
+
`activeTagKey` is not, `Active=true` is still required, so existing settings behave exactly as before.
|
|
45
|
+
3. **Explicit list.** `profiles` names ARNs or profile ids; each is read with `GetInferenceProfile` and no tag search runs.
|
|
38
46
|
|
|
39
47
|
| Setting | Default | Meaning |
|
|
40
48
|
| --------------------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
41
|
-
| `team` |
|
|
42
|
-
| `tagKey` | `Team`
|
|
43
|
-
| `tagValue` | the value of `team` | The tag value
|
|
44
|
-
| `activeTagKey` / `activeTagValue` |
|
|
49
|
+
| `team` | none | Shorthand for `tagValue`; existing settings keep the `Active=true` filter |
|
|
50
|
+
| `tagKey` | `Team` when tagged | The tag key for mode 2 |
|
|
51
|
+
| `tagValue` | the value of `team` | The tag value for mode 2 |
|
|
52
|
+
| `activeTagKey` / `activeTagValue` | none | Optional tag that marks a profile usable; `team` alone still defaults to `Active`/`true` |
|
|
53
|
+
| `profiles` | none | Mode 3: explicit inference-profile ARNs or ids |
|
|
45
54
|
| `profile` | `my-bedrock-profile` | The AWS profile for discovery, requests and sign-in |
|
|
46
55
|
| `regions` | `["us-east-1"]` | Where to look for profiles |
|
|
47
56
|
| `usageDir` | `~/.local/share/usage-tool` | Optional directory holding an executable `usage` that prints the usage JSON; omit if you have none |
|
|
@@ -62,13 +71,14 @@ when Pi starts an SSO check, or when you run a command.
|
|
|
62
71
|
|
|
63
72
|
### Discovery
|
|
64
73
|
|
|
65
|
-
Discovery runs the AWS CLI with the configured `profile` and `regions
|
|
74
|
+
Discovery runs the AWS CLI with the configured `profile` and `regions`. It always starts with
|
|
75
|
+
`aws sts get-caller-identity`. Then:
|
|
66
76
|
|
|
67
|
-
- `aws
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
`
|
|
71
|
-
- `
|
|
77
|
+
- **System:** `aws bedrock list-inference-profiles --type-equals SYSTEM_DEFINED`, paginated per region; only Claude
|
|
78
|
+
profiles are kept, then `GetInferenceProfile` reads each one.
|
|
79
|
+
- **Tag:** `aws resourcegroupstaggingapi get-resources --resource-type-filters
|
|
80
|
+
bedrock:application-inference-profile`, filtered by `tagKey`/`tagValue` and, only when configured, the active tag.
|
|
81
|
+
- **Explicit:** `GetInferenceProfile` for each `profiles` entry; no tag call and no system listing.
|
|
72
82
|
|
|
73
83
|
Only `ACTIVE` profiles are listed. Their foundation model is read from the profile's model ARN; the provider does not call
|
|
74
84
|
`GetFoundationModel`. A successful catalog is cached for **24 hours** in `~/.cache/jig-bedrock/catalog.json`, with private
|
|
@@ -84,8 +94,9 @@ There are two routes to the same Claude models:
|
|
|
84
94
|
configured profile.
|
|
85
95
|
- **Converse.** Pi's Bedrock provider uses the AWS SDK's `ConverseStream` operation at the bedrock-runtime endpoint.
|
|
86
96
|
|
|
87
|
-
A profile is put on Messages only when `api` is `messages` (the default)
|
|
88
|
-
|
|
97
|
+
A profile is put on Messages only when `api` is `messages` (the default), it is an **application** inference profile, and
|
|
98
|
+
its foundation model is an Anthropic model. System-defined profile ids use Converse, because they are not application
|
|
99
|
+
inference-profile ARNs. Every other model uses Converse. `api: "converse"` puts every model on Converse. Both routes stream. On Messages the tools array stays
|
|
89
100
|
stable, so the prompt cache survives a tool change; on Converse a changed tool list rewrites it. The footer and usage
|
|
90
101
|
figures are the same.
|
|
91
102
|
|
|
@@ -93,6 +104,19 @@ A child agent (a Pi session with `PI_SUBAGENT_ID`, or the older `PI_SUBAGENT_CHI
|
|
|
93
104
|
usage program and its events, and the sign-in at start. The parent does those; the child still sends requests and reads
|
|
94
105
|
the parent's cached catalog.
|
|
95
106
|
|
|
107
|
+
## IAM
|
|
108
|
+
|
|
109
|
+
| Mode | Discovery permissions | Invoke |
|
|
110
|
+
| -------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
111
|
+
| System | `bedrock:ListInferenceProfiles`, `bedrock:GetInferenceProfile` | `bedrock:InvokeModel*` on the profile and on each foundation model it routes to, in every routing region |
|
|
112
|
+
| Tag | `tag:GetResources`, `bedrock:GetInferenceProfile` | same |
|
|
113
|
+
| Explicit | `bedrock:GetInferenceProfile` | same |
|
|
114
|
+
|
|
115
|
+
Add `sts:GetCallerIdentity` for the account check. For an AWS-defined cross-region profile the policy must allow the
|
|
116
|
+
profile **and** the foundation model in every region it routes to; AWS's [inference-profile permissions][ip-iam] say the
|
|
117
|
+
same. Application profiles additionally need the profile's own ARN. [ip-iam]:
|
|
118
|
+
https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-prereq.html
|
|
119
|
+
|
|
96
120
|
## Credentials
|
|
97
121
|
|
|
98
122
|
Requests use the AWS SDK's default credential chain, loaded from Pi's install (`pi-host.ts` loads
|
|
@@ -180,3 +204,11 @@ Real-AWS tests are manual and need the user's approval; unit tests use recorded
|
|
|
180
204
|
## Licence
|
|
181
205
|
|
|
182
206
|
MIT.
|
|
207
|
+
|
|
208
|
+
## Not verified against real AWS
|
|
209
|
+
|
|
210
|
+
- The exact Messages behaviour for a system-defined profile id (this package sends those through Converse).
|
|
211
|
+
- The response shapes of `ListInferenceProfiles` pagination and system-profile model ARNs, which the tests fake from
|
|
212
|
+
AWS documentation.
|
|
213
|
+
- IAM policy evaluation for cross-region profile plus foundation-model permissions in every routing region.
|
|
214
|
+
- Pricing and model metadata for system-defined profiles come from Pi's registry; no live `GetFoundationModel` call is made.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clawdbuilder/jig-bedrock",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Claude on AWS Bedrock for Pi: inference-profile discovery, the Messages route with cache-friendly tools, Converse fallback, SSO repair and a budget footer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/aws-discovery.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { rmSync } from "node:fs";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { atomicCache } from "./cache.ts";
|
|
6
|
-
import { type Catalog, type Profile, readCatalog, validArn } from "./catalog.ts";
|
|
6
|
+
import { type Catalog, filterHash, type Profile, readCatalog, validArn } from "./catalog.ts";
|
|
7
7
|
import type { ResolvedConfig } from "./config.ts";
|
|
8
8
|
|
|
9
9
|
// biome-ignore lint/suspicious/noExplicitAny: parsed JSON from the AWS CLI or usage program, checked field by field where it is used
|
|
@@ -65,6 +65,22 @@ export function discoveryHelp(status: Status): string {
|
|
|
65
65
|
return "Check AWS access and the configuration in README.md, then /bedrock-refresh.";
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
+
const PROFILE_ERROR =
|
|
69
|
+
/profile scope|model scope|model ID|ambiguous model|models|profile name|profile mismatch|profile tags/;
|
|
70
|
+
|
|
71
|
+
function profileError(error: unknown): boolean {
|
|
72
|
+
return PROFILE_ERROR.test(String(error));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function entryMessage(error: unknown): string {
|
|
76
|
+
const text = String(error);
|
|
77
|
+
if (/profile scope/.test(text)) return text.replace(/^Error:\s*/, "");
|
|
78
|
+
if (/model scope/.test(text)) return "model scope";
|
|
79
|
+
if (category(error) === "permission_denied") return "denied";
|
|
80
|
+
if (category(error) === "configuration") return "not found";
|
|
81
|
+
return category(error);
|
|
82
|
+
}
|
|
83
|
+
|
|
68
84
|
export function createAwsDiscovery(
|
|
69
85
|
config: ResolvedConfig,
|
|
70
86
|
runner: Runner = runFile,
|
|
@@ -73,7 +89,13 @@ export function createAwsDiscovery(
|
|
|
73
89
|
): CatalogSource {
|
|
74
90
|
let flight: Promise<Status> | undefined,
|
|
75
91
|
invalidAccount = false;
|
|
76
|
-
const
|
|
92
|
+
const mode =
|
|
93
|
+
config.mode ??
|
|
94
|
+
(config.profiles?.length ? "explicit" : config.team || config.tagValue || config.tagKey ? "tag" : "system");
|
|
95
|
+
const activeKey = config.activeTagKey ?? (config.team !== undefined ? "Active" : undefined);
|
|
96
|
+
const activeValue = config.activeTagValue ?? "true";
|
|
97
|
+
const scope = { ...config, mode, team: config.team };
|
|
98
|
+
const load = () => (invalidAccount ? undefined : readCatalog(file, scope, now()));
|
|
77
99
|
const perform = async (force = false): Promise<Status> => {
|
|
78
100
|
let remaining = 8 * 1_048_576,
|
|
79
101
|
pages = 0,
|
|
@@ -92,20 +114,21 @@ export function createAwsDiscovery(
|
|
|
92
114
|
return JSON.parse(output);
|
|
93
115
|
};
|
|
94
116
|
try {
|
|
95
|
-
// A catalog younger than the TTL needs no AWS call at all, not even to learn who the caller is; /bedrock-refresh forces one.
|
|
96
117
|
if (!force && !invalidAccount) {
|
|
97
|
-
const cached = readCatalog(file,
|
|
118
|
+
const cached = readCatalog(file, scope, now());
|
|
98
119
|
if (cached && now() / 1000 - cached.fetched_at < 86_400) return "ok";
|
|
99
120
|
}
|
|
100
121
|
const account = (await aws(["sts", "get-caller-identity"])).Account;
|
|
101
122
|
if (typeof account !== "string" || !/^\d{12}$/.test(account)) throw new Error("identity");
|
|
102
|
-
const old = readCatalog(file,
|
|
123
|
+
const old = readCatalog(file, scope, now());
|
|
103
124
|
if (old && old.account_id !== account) {
|
|
104
125
|
invalidAccount = true;
|
|
105
126
|
rmSync(file, { force: true });
|
|
106
127
|
} else invalidAccount = false;
|
|
107
128
|
const profiles: Profile[] = [],
|
|
108
|
-
seen = new Set<string>()
|
|
129
|
+
seen = new Set<string>(),
|
|
130
|
+
explicitErrors: { profile: string; message: string }[] = [],
|
|
131
|
+
warnings: string[] = [];
|
|
109
132
|
const info = (arn: string, region: string) =>
|
|
110
133
|
aws(["bedrock", "get-inference-profile", "--inference-profile-identifier", arn, "--region", region]);
|
|
111
134
|
const foundation = async (value: Json, depth = 0): Promise<string> => {
|
|
@@ -123,95 +146,184 @@ export function createAwsDiscovery(
|
|
|
123
146
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: reject control characters in an AWS model id
|
|
124
147
|
if (!id || id.length > 256 || /[\x00-\x1f]/.test(id)) throw new Error("model ID");
|
|
125
148
|
ids.add(id);
|
|
126
|
-
} else if (
|
|
127
|
-
|
|
128
|
-
depth < 2 &&
|
|
129
|
-
(!match[3] || match[3] === account)
|
|
130
|
-
)
|
|
149
|
+
} else if (resource.startsWith("inference-profile/") && depth < 2) {
|
|
150
|
+
if (match[3] && match[3] !== account && match[3] !== "aws") throw new Error("model scope");
|
|
131
151
|
ids.add(await foundation(await info(row.modelArn, match[2]), depth + 1));
|
|
132
|
-
else throw new Error("model scope");
|
|
152
|
+
} else throw new Error("model scope");
|
|
133
153
|
}
|
|
134
154
|
if (ids.size !== 1) throw new Error("ambiguous model");
|
|
135
155
|
return [...ids][0] as string;
|
|
136
156
|
};
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
157
|
+
const nameOf = (value: Json): string => {
|
|
158
|
+
const name = value.inferenceProfileName;
|
|
159
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: reject control characters in an AWS profile name
|
|
160
|
+
if (typeof name !== "string" || !name || name.length > 256 || /[\x00-\x1f\x7f]/.test(name))
|
|
161
|
+
throw new Error("profile name");
|
|
162
|
+
return name;
|
|
163
|
+
};
|
|
164
|
+
const add = async (candidate: string, region: string, kind: "application" | "system") => {
|
|
165
|
+
if (!validArn(candidate, region, account, kind) || seen.has(candidate) || seen.size >= 256)
|
|
166
|
+
throw new Error("profile scope");
|
|
167
|
+
const value = await info(candidate, region);
|
|
168
|
+
if (value.inferenceProfileArn !== undefined && value.inferenceProfileArn !== candidate)
|
|
169
|
+
throw new Error("profile mismatch");
|
|
170
|
+
if (value.status !== "ACTIVE") return;
|
|
171
|
+
seen.add(candidate);
|
|
172
|
+
const foundationModelId = await foundation(value);
|
|
173
|
+
if (kind === "system" && !foundationModelId.startsWith("anthropic.claude")) return;
|
|
174
|
+
profiles.push({
|
|
175
|
+
arn: candidate,
|
|
176
|
+
profile_name: nameOf(value),
|
|
177
|
+
foundation_model_id: foundationModelId,
|
|
178
|
+
region,
|
|
179
|
+
kind,
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
if (mode === "explicit") {
|
|
183
|
+
for (const entry of config.profiles ?? []) {
|
|
184
|
+
const match = entry.match(
|
|
185
|
+
/^arn:(aws|aws-cn|aws-us-gov):bedrock:([^:]+):([^:]*):(application-inference-profile|inference-profile)\/(.+)$/,
|
|
186
|
+
);
|
|
187
|
+
const kind = (match?.[4] === "inference-profile" ? "system" : "application") as
|
|
188
|
+
| "application"
|
|
189
|
+
| "system";
|
|
190
|
+
const region = match?.[2] ?? config.regions[0];
|
|
191
|
+
if (!region) throw new Error("profile scope");
|
|
192
|
+
try {
|
|
193
|
+
if (match && !config.regions.includes(region))
|
|
194
|
+
throw new Error(
|
|
195
|
+
`profile scope: ${entry} is in ${region}; add ${region} to bedrock.regions`,
|
|
196
|
+
);
|
|
197
|
+
if (match) await add(entry, region, kind);
|
|
198
|
+
else {
|
|
199
|
+
const found = await info(entry, region);
|
|
200
|
+
const foundArn = found.inferenceProfileArn;
|
|
201
|
+
if (typeof foundArn !== "string") throw new Error("profile ARN");
|
|
202
|
+
await add(foundArn, region, found.type === "SYSTEM_DEFINED" ? "system" : "application");
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (!profileError(error) && !/AccessDenied/i.test(String(error))) throw error;
|
|
206
|
+
explicitErrors.push({ profile: entry, message: entryMessage(error) });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} else if (mode === "tag") {
|
|
210
|
+
for (const region of config.regions) {
|
|
211
|
+
let token = "";
|
|
212
|
+
const tokens = new Set<string>();
|
|
213
|
+
for (let page = 0; page < 32; page++) {
|
|
214
|
+
if (++pages > 64) throw new Error("pagination");
|
|
215
|
+
const result = await aws([
|
|
216
|
+
"resourcegroupstaggingapi",
|
|
217
|
+
"get-resources",
|
|
218
|
+
"--region",
|
|
219
|
+
region,
|
|
220
|
+
"--resource-type-filters",
|
|
221
|
+
"bedrock:application-inference-profile",
|
|
222
|
+
"--tag-filters",
|
|
223
|
+
JSON.stringify([
|
|
224
|
+
{ Key: config.tagKey ?? "Team", Values: [config.tagValue ?? config.team] },
|
|
225
|
+
...(activeKey === undefined ? [] : [{ Key: activeKey, Values: [activeValue] }]),
|
|
226
|
+
]),
|
|
227
|
+
"--no-paginate",
|
|
228
|
+
...(token ? ["--pagination-token", token] : []),
|
|
229
|
+
]);
|
|
230
|
+
if (!Array.isArray(result.ResourceTagMappingList) || result.ResourceTagMappingList.length > 256)
|
|
231
|
+
throw new Error("resources");
|
|
232
|
+
for (const row of result.ResourceTagMappingList) {
|
|
233
|
+
const arn = row.ResourceARN;
|
|
234
|
+
if (
|
|
235
|
+
typeof arn !== "string" ||
|
|
236
|
+
!validArn(arn, region, account) ||
|
|
237
|
+
seen.has(arn) ||
|
|
238
|
+
seen.size >= 256
|
|
239
|
+
)
|
|
240
|
+
throw new Error("profile scope");
|
|
241
|
+
const wanted = [
|
|
171
242
|
[config.tagKey ?? "Team", config.tagValue ?? config.team],
|
|
172
|
-
[
|
|
173
|
-
]
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
243
|
+
...(activeKey === undefined ? [] : [[activeKey, activeValue]]),
|
|
244
|
+
] as Array<[string, string | undefined]>;
|
|
245
|
+
if (
|
|
246
|
+
!Array.isArray(row.Tags) ||
|
|
247
|
+
!wanted.every(
|
|
248
|
+
([key, value]) =>
|
|
249
|
+
row.Tags.filter((tag: Json) => tag?.Key === key).length === 1 &&
|
|
250
|
+
row.Tags.some((tag: Json) => tag?.Key === key && tag.Value === value),
|
|
251
|
+
)
|
|
177
252
|
)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
253
|
+
throw new Error("profile tags");
|
|
254
|
+
await add(arn, region, "application");
|
|
255
|
+
}
|
|
256
|
+
token = result.PaginationToken ?? "";
|
|
257
|
+
if (!token) break;
|
|
258
|
+
if (typeof token !== "string" || token.length > 2048 || tokens.has(token) || page === 31)
|
|
259
|
+
throw new Error("pagination");
|
|
260
|
+
tokens.add(token);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
for (const region of config.regions) {
|
|
265
|
+
let token = "";
|
|
266
|
+
const tokens = new Set<string>();
|
|
267
|
+
for (let page = 0; page < 32; page++) {
|
|
268
|
+
if (++pages > 64) throw new Error("pagination");
|
|
269
|
+
const result = await aws([
|
|
270
|
+
"bedrock",
|
|
271
|
+
"list-inference-profiles",
|
|
272
|
+
"--type-equals",
|
|
273
|
+
"SYSTEM_DEFINED",
|
|
274
|
+
"--region",
|
|
193
275
|
region,
|
|
194
|
-
|
|
276
|
+
...(token ? ["--next-token", token] : []),
|
|
277
|
+
]);
|
|
278
|
+
const rows = result.inferenceProfileSummaries;
|
|
279
|
+
if (!Array.isArray(rows) || rows.length > 256) throw new Error("profiles");
|
|
280
|
+
for (const row of rows) {
|
|
281
|
+
const arn = row?.inferenceProfileArn;
|
|
282
|
+
if (typeof arn !== "string") throw new Error("profile scope");
|
|
283
|
+
if (!/anthropic\.claude/i.test(`${arn} ${row.inferenceProfileName ?? ""}`)) continue;
|
|
284
|
+
try {
|
|
285
|
+
await add(arn, region, "system");
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if (!profileError(error)) throw error;
|
|
288
|
+
explicitErrors.push({ profile: arn, message: entryMessage(error) });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
token = result.nextToken ?? "";
|
|
292
|
+
if (!token) break;
|
|
293
|
+
if (typeof token !== "string" || token.length > 2048 || tokens.has(token) || page === 31)
|
|
294
|
+
throw new Error("pagination");
|
|
295
|
+
tokens.add(token);
|
|
195
296
|
}
|
|
196
|
-
token = result.PaginationToken ?? "";
|
|
197
|
-
if (!token) break;
|
|
198
|
-
if (typeof token !== "string" || token.length > 2048 || tokens.has(token) || page === 31)
|
|
199
|
-
throw new Error("pagination");
|
|
200
|
-
tokens.add(token);
|
|
201
297
|
}
|
|
202
298
|
}
|
|
203
299
|
profiles.sort((a, b) => (a.arn < b.arn ? -1 : a.arn > b.arn ? 1 : 0));
|
|
204
|
-
|
|
300
|
+
const failed =
|
|
301
|
+
(mode === "explicit" || mode === "system") && profiles.length === 0 && explicitErrors.length > 0;
|
|
302
|
+
if (mode === "tag" && profiles.length === 0) {
|
|
303
|
+
const active = activeKey === undefined ? "" : ` and ${activeKey}=${activeValue}`;
|
|
304
|
+
warnings.push(
|
|
305
|
+
`No application inference profiles matched ${config.tagKey ?? "Team"}=${config.tagValue}${active} in ${config.regions.join(", ")}. Check bedrock.tagKey/tagValue.`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
if (mode === "system" && profiles.length === 0)
|
|
309
|
+
warnings.push(`No AWS-defined Claude inference profiles in ${config.regions.join(", ")}.`);
|
|
310
|
+
if (failed) return "unavailable";
|
|
311
|
+
const catalog: Catalog = {
|
|
205
312
|
schema: 1,
|
|
313
|
+
mode,
|
|
206
314
|
team: config.team,
|
|
207
315
|
aws_profile: config.profile,
|
|
208
316
|
account_id: account,
|
|
209
317
|
regions: config.regions,
|
|
210
318
|
fetched_at: Math.floor(now() / 1000),
|
|
319
|
+
filter_hash: filterHash(scope),
|
|
320
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
211
321
|
profiles,
|
|
212
|
-
|
|
322
|
+
...(explicitErrors.length > 0 ? { errors: explicitErrors } : {}),
|
|
323
|
+
};
|
|
324
|
+
atomicCache(file, catalog);
|
|
213
325
|
invalidAccount = false;
|
|
214
|
-
return "ok";
|
|
326
|
+
return failed ? "unavailable" : "ok";
|
|
215
327
|
} catch (e) {
|
|
216
328
|
return category(e);
|
|
217
329
|
}
|
package/src/catalog.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, readSync } from "node:fs";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -54,15 +55,26 @@ const MESSAGES_COMPAT = {
|
|
|
54
55
|
supportsMidConvoSystemMessages: true,
|
|
55
56
|
supportsMidConvoToolChanges: true,
|
|
56
57
|
} as const;
|
|
57
|
-
export type Profile = {
|
|
58
|
+
export type Profile = {
|
|
59
|
+
arn: string;
|
|
60
|
+
profile_name: string;
|
|
61
|
+
foundation_model_id: string;
|
|
62
|
+
region: string;
|
|
63
|
+
kind?: "application" | "system";
|
|
64
|
+
};
|
|
58
65
|
export type Catalog = {
|
|
59
66
|
schema: 1;
|
|
60
|
-
|
|
67
|
+
mode?: "explicit" | "tag" | "system";
|
|
68
|
+
team?: string;
|
|
61
69
|
aws_profile: string;
|
|
62
70
|
account_id: string;
|
|
63
71
|
regions: string[];
|
|
64
72
|
fetched_at: number;
|
|
73
|
+
filter_hash?: string;
|
|
74
|
+
warnings?: string[];
|
|
65
75
|
profiles: Profile[];
|
|
76
|
+
/** Explicit-list entries that could not be read; they do not stop the others. */
|
|
77
|
+
errors?: { profile: string; message: string }[];
|
|
66
78
|
};
|
|
67
79
|
const slug = (s: string) =>
|
|
68
80
|
s
|
|
@@ -92,27 +104,68 @@ export function boundedRegular(file: string, max: number, privateMode = false):
|
|
|
92
104
|
closeSync(fd);
|
|
93
105
|
}
|
|
94
106
|
}
|
|
95
|
-
export function validArn(
|
|
107
|
+
export function validArn(
|
|
108
|
+
arn: string,
|
|
109
|
+
region: string,
|
|
110
|
+
account: string,
|
|
111
|
+
kind: "application" | "system" | "any" = "application",
|
|
112
|
+
): boolean {
|
|
96
113
|
const p = arn.split(":");
|
|
114
|
+
const app = (p[5] ?? "").startsWith("application-inference-profile/");
|
|
115
|
+
const system = (p[5] ?? "").startsWith("inference-profile/");
|
|
97
116
|
return (
|
|
98
117
|
p.length === 6 &&
|
|
99
118
|
p[0] === "arn" &&
|
|
100
119
|
["aws", "aws-cn", "aws-us-gov"].includes(p[1] ?? "") &&
|
|
101
120
|
p[2] === "bedrock" &&
|
|
102
121
|
p[3] === region &&
|
|
103
|
-
p[4] === account &&
|
|
104
|
-
|
|
122
|
+
(system ? p[4] === "" || p[4] === "aws" || p[4] === account : p[4] === account) &&
|
|
123
|
+
(kind === "any" ? app || system : kind === "application" ? app : system) &&
|
|
124
|
+
/^(?:application-inference-profile|inference-profile)\/[A-Za-z0-9.-]{1,256}$/.test(p[5] ?? "")
|
|
105
125
|
);
|
|
106
126
|
}
|
|
127
|
+
/** The discovery filter, hashed into the catalog so a changed tag or list is never reused. */
|
|
128
|
+
export function filterHash(expected: {
|
|
129
|
+
mode?: "explicit" | "tag" | "system";
|
|
130
|
+
tagKey?: string;
|
|
131
|
+
tagValue?: string;
|
|
132
|
+
activeTagKey?: string;
|
|
133
|
+
activeTagValue?: string;
|
|
134
|
+
profiles?: string[];
|
|
135
|
+
}): string {
|
|
136
|
+
return createHash("sha256")
|
|
137
|
+
.update(
|
|
138
|
+
JSON.stringify([
|
|
139
|
+
expected.mode ?? null,
|
|
140
|
+
expected.tagKey ?? null,
|
|
141
|
+
expected.tagValue ?? null,
|
|
142
|
+
expected.activeTagKey ?? null,
|
|
143
|
+
expected.activeTagValue ?? null,
|
|
144
|
+
expected.profiles ?? null,
|
|
145
|
+
]),
|
|
146
|
+
)
|
|
147
|
+
.digest("hex");
|
|
148
|
+
}
|
|
149
|
+
|
|
107
150
|
export function readCatalog(
|
|
108
151
|
file: string,
|
|
109
|
-
expected: {
|
|
152
|
+
expected: {
|
|
153
|
+
mode?: "explicit" | "tag" | "system";
|
|
154
|
+
team?: string;
|
|
155
|
+
tagKey?: string;
|
|
156
|
+
tagValue?: string;
|
|
157
|
+
activeTagKey?: string;
|
|
158
|
+
activeTagValue?: string;
|
|
159
|
+
profiles?: string[];
|
|
160
|
+
profile: string;
|
|
161
|
+
regions: string[];
|
|
162
|
+
},
|
|
110
163
|
now = Date.now(),
|
|
111
164
|
): Catalog | undefined {
|
|
112
165
|
expected = { ...expected, regions: [...expected.regions].sort() };
|
|
113
166
|
const directory = dirname(file);
|
|
114
167
|
if (
|
|
115
|
-
!label(expected.team, 256) ||
|
|
168
|
+
(expected.team !== undefined && !label(expected.team, 256)) ||
|
|
116
169
|
!label(expected.profile, 128) ||
|
|
117
170
|
!expected.regions.length ||
|
|
118
171
|
new Set(expected.regions).size !== expected.regions.length
|
|
@@ -127,6 +180,9 @@ export function readCatalog(
|
|
|
127
180
|
if (
|
|
128
181
|
c.schema !== 1 ||
|
|
129
182
|
c.team !== expected.team ||
|
|
183
|
+
(c.mode === undefined
|
|
184
|
+
? expected.mode !== "tag" || c.filter_hash !== undefined
|
|
185
|
+
: c.mode !== expected.mode || c.filter_hash !== filterHash(expected)) ||
|
|
130
186
|
c.aws_profile !== expected.profile ||
|
|
131
187
|
typeof c.account_id !== "string" ||
|
|
132
188
|
!/^\d{12}$/.test(c.account_id) ||
|
|
@@ -151,7 +207,7 @@ export function readCatalog(
|
|
|
151
207
|
!label(p.profile_name, 256) ||
|
|
152
208
|
!label(p.foundation_model_id, 256) ||
|
|
153
209
|
typeof p.arn !== "string" ||
|
|
154
|
-
!validArn(p.arn, p.region, c.account_id) ||
|
|
210
|
+
!validArn(p.arn, p.region, c.account_id, p.kind ?? "application") ||
|
|
155
211
|
seen.has(p.arn)
|
|
156
212
|
)
|
|
157
213
|
return;
|
|
@@ -198,6 +254,42 @@ function thinkingLevelMap(
|
|
|
198
254
|
return { thinkingLevelMap: { ...(base ?? {}), off: null } };
|
|
199
255
|
}
|
|
200
256
|
|
|
257
|
+
/** System profiles first, by the geography of their id: the region's prefix, then global, then the rest. */
|
|
258
|
+
function orderedSystem(rows: Profile[], catalog: Catalog | undefined): Profile[] {
|
|
259
|
+
const firstRegion = catalog?.regions?.[0] ?? "us-east-1";
|
|
260
|
+
const first = /^us-gov-/.test(firstRegion)
|
|
261
|
+
? "us-gov"
|
|
262
|
+
: /^ap-northeast-(?:1|3)$/.test(firstRegion)
|
|
263
|
+
? "jp"
|
|
264
|
+
: /^ap-southeast-(?:2|4)$/.test(firstRegion)
|
|
265
|
+
? "au"
|
|
266
|
+
: /^ap-/.test(firstRegion)
|
|
267
|
+
? "apac"
|
|
268
|
+
: (firstRegion.split("-")[0] ?? "us");
|
|
269
|
+
const geography = (p: Profile) => {
|
|
270
|
+
const id = p.arn.slice(p.arn.lastIndexOf("/") + 1);
|
|
271
|
+
const prefix = id.split(".")[0] ?? "";
|
|
272
|
+
return prefix;
|
|
273
|
+
};
|
|
274
|
+
const order = catalog?.regions ?? [firstRegion];
|
|
275
|
+
const rank = (p: Profile): number => {
|
|
276
|
+
const prefix = geography(p);
|
|
277
|
+
if (p.kind !== "system") return 999;
|
|
278
|
+
if (prefix === first) return 0;
|
|
279
|
+
if (prefix === "global") return 1;
|
|
280
|
+
return 2 + Math.max(0, order.indexOf(p.region));
|
|
281
|
+
};
|
|
282
|
+
return rows
|
|
283
|
+
.filter((p) => p.kind === "system")
|
|
284
|
+
.sort((a, b) => rank(a) - rank(b) || (a.arn < b.arn ? -1 : a.arn > b.arn ? 1 : 0));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function orderedApplication(rows: Profile[]): Profile[] {
|
|
288
|
+
const system = rows.filter((p) => p.kind === "system");
|
|
289
|
+
const apps = rows.filter((p) => p.kind !== "system");
|
|
290
|
+
return [...system, ...apps];
|
|
291
|
+
}
|
|
292
|
+
|
|
201
293
|
export function discoveredProfiles(
|
|
202
294
|
catalog: Catalog | undefined,
|
|
203
295
|
usageDir: string,
|
|
@@ -228,10 +320,12 @@ export function discoveredProfiles(
|
|
|
228
320
|
const key = slug(registryName(r.name));
|
|
229
321
|
grouped.set(key, [...(grouped.get(key) || []), p]);
|
|
230
322
|
}
|
|
231
|
-
for (const [key, rows] of grouped)
|
|
232
|
-
|
|
323
|
+
for (const [key, rows] of grouped) {
|
|
324
|
+
const system = orderedSystem(rows, catalog);
|
|
325
|
+
const preferred = system[0] ?? (rows.length === 1 ? rows[0] : undefined);
|
|
326
|
+
for (const p of orderedApplication(rows)) {
|
|
233
327
|
const r = registryEntry(p.foundation_model_id);
|
|
234
|
-
const id =
|
|
328
|
+
const id = p === preferred ? key : `${key}--${p.region}--${p.arn.slice(p.arn.lastIndexOf("/") + 1)}`;
|
|
235
329
|
const pricing = rates.get(key);
|
|
236
330
|
const fallback =
|
|
237
331
|
r.cost &&
|
|
@@ -250,13 +344,13 @@ export function discoveredProfiles(
|
|
|
250
344
|
}
|
|
251
345
|
const promptCache = promptCacheFor(p.foundation_model_id);
|
|
252
346
|
// Only Claude profiles go on the Anthropic route; gpt, nova and the rest keep their own route.
|
|
253
|
-
const messages = api === "messages" && isAnthropicModel(p.foundation_model_id);
|
|
347
|
+
const messages = api === "messages" && p.kind !== "system" && isAnthropicModel(p.foundation_model_id);
|
|
254
348
|
available.push({
|
|
255
349
|
id,
|
|
256
350
|
arn: p.arn,
|
|
257
351
|
foundationModelId: p.foundation_model_id,
|
|
258
352
|
aliases: [],
|
|
259
|
-
name: registryName(r.name) + (
|
|
353
|
+
name: registryName(r.name) + (p === preferred ? "" : ` (${p.region}, ${p.profile_name})`),
|
|
260
354
|
api: messages ? "anthropic-messages" : "bedrock-converse-stream",
|
|
261
355
|
baseUrl: messages
|
|
262
356
|
? `https://bedrock-runtime.${p.region}.amazonaws.com/anthropic`
|
|
@@ -280,6 +374,7 @@ export function discoveredProfiles(
|
|
|
280
374
|
...(thinkingLevelMap(r.thinkingLevelMap, p.foundation_model_id) ?? {}),
|
|
281
375
|
});
|
|
282
376
|
}
|
|
377
|
+
}
|
|
283
378
|
return { available, unavailable };
|
|
284
379
|
}
|
|
285
380
|
export function updateModels(
|
package/src/commands.ts
CHANGED
|
@@ -276,7 +276,7 @@ export function registerStatisticsCommands(
|
|
|
276
276
|
},
|
|
277
277
|
});
|
|
278
278
|
pi.registerCommand("bedrock-profiles", {
|
|
279
|
-
description: "Show approved
|
|
279
|
+
description: "Show your approved Bedrock models, regions and pricing sources from the current catalog",
|
|
280
280
|
handler: async (args, ctx) => {
|
|
281
281
|
if (args.trim()) {
|
|
282
282
|
ctx.ui.notify("Usage: /bedrock-profiles", "warning");
|
|
@@ -284,16 +284,20 @@ export function registerStatisticsCommands(
|
|
|
284
284
|
}
|
|
285
285
|
await showReport(
|
|
286
286
|
ctx,
|
|
287
|
-
"Bedrock ·
|
|
287
|
+
"Bedrock · Your profiles",
|
|
288
288
|
async () => {
|
|
289
289
|
const catalog = sources.catalog.load(),
|
|
290
290
|
models = sources.models();
|
|
291
291
|
return [
|
|
292
|
-
`
|
|
292
|
+
`${config.mode === "system" ? "Mode: system-defined" : config.mode === "explicit" ? "Mode: explicit list" : `Tag: ${clean(config.tagKey ?? "Team")}=${clean(config.tagValue)}`} · AWS profile: ${clean(config.profile)} · regions: ${config.regions.join(", ")}`,
|
|
293
293
|
catalog
|
|
294
294
|
? `Catalog observed ${new Date(catalog.fetched_at * 1000).toISOString()}${Date.now() / 1000 - catalog.fetched_at > 86_400 ? " (stale; /bedrock-refresh)" : ""}. ${catalog.profiles.length} active profiles / ${models.length} usable models.`
|
|
295
295
|
: "No approved catalog. Run /aws-login, then /bedrock-refresh.",
|
|
296
296
|
"Catalog tags filter presentation; AWS IAM authorizes model access.",
|
|
297
|
+
...(catalog?.warnings ?? []).map((line) => `Note: ${line}`),
|
|
298
|
+
...(catalog?.errors ?? []).map(
|
|
299
|
+
(error) => `Could not read ${clean(error.profile)}: ${clean(error.message)}.`,
|
|
300
|
+
),
|
|
297
301
|
...models.map(
|
|
298
302
|
(m) =>
|
|
299
303
|
`${clean(m.name)} · ${m.arn.split(":")[3]} · ${m.rateSource} / million tokens: input $${m.cost.input}, output $${m.cost.output}, cache read $${m.cost.cacheRead}, ${writeRates(m)}`,
|
package/src/config.ts
CHANGED
|
@@ -19,11 +19,15 @@ import type { Check } from "./check.ts";
|
|
|
19
19
|
import { isObject, knownKeys } from "./check.ts";
|
|
20
20
|
|
|
21
21
|
export interface ResolvedConfig {
|
|
22
|
-
team
|
|
22
|
+
team?: string;
|
|
23
23
|
tagKey?: string;
|
|
24
24
|
tagValue?: string;
|
|
25
25
|
activeTagKey?: string;
|
|
26
26
|
activeTagValue?: string;
|
|
27
|
+
/** Explicit inference-profile ARNs or ids; when nonempty, no tag or system search runs. */
|
|
28
|
+
profiles?: string[];
|
|
29
|
+
/** Derived: explicit, tag or system. */
|
|
30
|
+
mode?: "explicit" | "tag" | "system";
|
|
27
31
|
profile: string;
|
|
28
32
|
regions: string[];
|
|
29
33
|
usageDir: string;
|
|
@@ -52,6 +56,7 @@ export const FIELDS = [
|
|
|
52
56
|
"tagValue",
|
|
53
57
|
"activeTagKey",
|
|
54
58
|
"activeTagValue",
|
|
59
|
+
"profiles",
|
|
55
60
|
"profile",
|
|
56
61
|
"regions",
|
|
57
62
|
"usageDir",
|
|
@@ -63,9 +68,6 @@ export const FIELDS = [
|
|
|
63
68
|
] as const;
|
|
64
69
|
|
|
65
70
|
export const DEFAULTS = {
|
|
66
|
-
tagKey: "Team",
|
|
67
|
-
activeTagKey: "Active",
|
|
68
|
-
activeTagValue: "true",
|
|
69
71
|
profile: "my-bedrock-profile",
|
|
70
72
|
regions: ["us-east-1"],
|
|
71
73
|
usageDir: "~/.local/share/usage-tool",
|
|
@@ -86,6 +88,7 @@ export function problemsOf(c: Record<string, unknown>): string[] {
|
|
|
86
88
|
tagValue,
|
|
87
89
|
activeTagKey,
|
|
88
90
|
activeTagValue,
|
|
91
|
+
profiles,
|
|
89
92
|
profile,
|
|
90
93
|
regions,
|
|
91
94
|
usageDir,
|
|
@@ -95,7 +98,10 @@ export function problemsOf(c: Record<string, unknown>): string[] {
|
|
|
95
98
|
browser,
|
|
96
99
|
api,
|
|
97
100
|
} = c;
|
|
98
|
-
if (
|
|
101
|
+
if (
|
|
102
|
+
team !== undefined &&
|
|
103
|
+
(typeof team !== "string" || team.trim() === "" || team.length > 256 || control.test(team))
|
|
104
|
+
) {
|
|
99
105
|
problems.push("bedrock.team must be your exact AWS tag value (1-256 characters, no control characters)");
|
|
100
106
|
}
|
|
101
107
|
for (const [name, value] of [
|
|
@@ -109,6 +115,24 @@ export function problemsOf(c: Record<string, unknown>): string[] {
|
|
|
109
115
|
(typeof value !== "string" || value.trim() === "" || value.length > 256 || control.test(value))
|
|
110
116
|
)
|
|
111
117
|
problems.push(`bedrock.${name} must be a tag name or value of 1-256 characters`);
|
|
118
|
+
if (
|
|
119
|
+
profiles !== undefined &&
|
|
120
|
+
(!Array.isArray(profiles) ||
|
|
121
|
+
profiles.length === 0 ||
|
|
122
|
+
profiles.length > 256 ||
|
|
123
|
+
profiles.some(
|
|
124
|
+
(value) =>
|
|
125
|
+
typeof value !== "string" ||
|
|
126
|
+
value.trim() === "" ||
|
|
127
|
+
value.length > 2048 ||
|
|
128
|
+
value.startsWith("-") ||
|
|
129
|
+
!/^[A-Za-z0-9._:/-]+$/.test(value) ||
|
|
130
|
+
control.test(value),
|
|
131
|
+
) ||
|
|
132
|
+
new Set(profiles).size !== profiles.length)
|
|
133
|
+
) {
|
|
134
|
+
problems.push("bedrock.profiles must be a nonempty list of up to 256 unique inference-profile ARNs or ids");
|
|
135
|
+
}
|
|
112
136
|
if (typeof profile !== "string" || !profilePattern.test(profile)) {
|
|
113
137
|
problems.push(
|
|
114
138
|
"bedrock.profile must be an AWS profile name: 1-128 letters, digits, underscores, dots or hyphens, starting with a letter or digit",
|
|
@@ -200,15 +224,20 @@ export function loadConfig(
|
|
|
200
224
|
if (env.JIG_BEDROCK_REGIONS !== undefined)
|
|
201
225
|
c.regions = env.JIG_BEDROCK_REGIONS.split(",").map((region) => region.trim());
|
|
202
226
|
const [problem] = problemsOf(c);
|
|
203
|
-
if (problem !== undefined) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
227
|
+
if (problem !== undefined) return { error: `${problem}.` };
|
|
228
|
+
const team = typeof c.team === "string" && c.team !== "" ? c.team : undefined;
|
|
229
|
+
const tagValue = typeof c.tagValue === "string" && c.tagValue !== "" ? c.tagValue : team;
|
|
230
|
+
const explicit = Array.isArray(c.profiles) && c.profiles.length > 0;
|
|
231
|
+
c.team = team;
|
|
232
|
+
c.tagValue = tagValue;
|
|
233
|
+
c.mode = explicit ? "explicit" : tagValue !== undefined || c.tagKey !== undefined ? "tag" : "system";
|
|
234
|
+
// Legacy team settings keep Active=true unless the user sets an active tag key.
|
|
235
|
+
if (c.mode === "tag" && c.activeTagKey === undefined && team !== undefined)
|
|
236
|
+
c.activeTagValue = c.activeTagValue ?? "true";
|
|
237
|
+
if (c.mode !== "tag") {
|
|
238
|
+
delete c.tagKey;
|
|
239
|
+
delete c.activeTagKey;
|
|
240
|
+
delete c.activeTagValue;
|
|
210
241
|
}
|
|
211
|
-
(c.regions as string[]).sort();
|
|
212
|
-
c.tagValue = typeof c.tagValue === "string" && c.tagValue !== "" ? c.tagValue : c.team;
|
|
213
242
|
return { config: c as unknown as ResolvedConfig };
|
|
214
243
|
}
|
package/src/provider.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The Bedrock model provider:
|
|
1
|
+
// The Bedrock model provider: your inference profiles as Pi models, AWS SSO recovery, a monthly spend figure and
|
|
2
2
|
// the commands around them. Its settings are the `bedrock` section when Jig is present, or the extension's own settings (see config.ts); it also
|
|
3
3
|
// publishes the optional display events in events.ts.
|
|
4
4
|
//
|
|
@@ -150,10 +150,7 @@ export function createJigBedrock(
|
|
|
150
150
|
}
|
|
151
151
|
if (!config) {
|
|
152
152
|
const file = path.join(agentFolder(options.env), "bedrock.json");
|
|
153
|
-
const where =
|
|
154
|
-
options.api === undefined
|
|
155
|
-
? `Add "team" to ${file}, then /reload.`
|
|
156
|
-
: `Set it in bedrock in jig.json or in ${file}, then /reload.`;
|
|
153
|
+
const where = `Fix bedrock in jig.json or in ${file}, then /reload.`;
|
|
157
154
|
pi.on("session_start", (_event, ctx) =>
|
|
158
155
|
ctx.ui.notify(`Bedrock provider disabled: ${resolved.error} ${where}`, "warning"),
|
|
159
156
|
);
|
|
@@ -258,11 +255,17 @@ export function createJigBedrock(
|
|
|
258
255
|
}
|
|
259
256
|
}
|
|
260
257
|
const change = updateModels(models, updated);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
258
|
+
const current = catalog.load();
|
|
259
|
+
const problems = current?.errors ?? [];
|
|
260
|
+
const warnings = current?.warnings ?? [];
|
|
261
|
+
if (ctx && (change.added || change.removed || problems.length > 0 || warnings.length > 0)) {
|
|
262
|
+
const detail = [
|
|
263
|
+
`Bedrock catalog updated: ${change.added} added, ${change.removed} removed.`,
|
|
264
|
+
...warnings,
|
|
265
|
+
...problems.map((problem) => `${problem.profile}: ${problem.message}.`),
|
|
266
|
+
"Choose /model if your selection was removed.",
|
|
267
|
+
].join(" ");
|
|
268
|
+
ctx.ui.notify(detail, problems.length > 0 || warnings.length > 0 ? "warning" : "info");
|
|
266
269
|
}
|
|
267
270
|
if (change.changed && native) {
|
|
268
271
|
await registerTeamBedrock(
|
|
@@ -290,7 +293,7 @@ export function createJigBedrock(
|
|
|
290
293
|
});
|
|
291
294
|
}
|
|
292
295
|
pi.registerCommand("bedrock-refresh", {
|
|
293
|
-
description: "Refresh
|
|
296
|
+
description: "Refresh your Bedrock models",
|
|
294
297
|
handler: async (_args, context) => {
|
|
295
298
|
const status = await refresh();
|
|
296
299
|
context.ui.notify(
|