@actagent/amazon-bedrock-mantle-provider 2026.6.2

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/discovery.ts ADDED
@@ -0,0 +1,436 @@
1
+ /**
2
+ * Amazon Bedrock Mantle discovery and bearer-token handling. It resolves
3
+ * explicit tokens, IAM-generated tokens, model catalogs, and implicit provider config.
4
+ */
5
+ import { createSubsystemLogger } from "actagent/plugin-sdk/core";
6
+ import { formatErrorMessage } from "actagent/plugin-sdk/error-runtime";
7
+ import {
8
+ isFutureDateTimestampMs,
9
+ resolveExpiresAtMsFromDurationMs,
10
+ } from "actagent/plugin-sdk/number-runtime";
11
+ import type {
12
+ ModelDefinitionConfig,
13
+ ModelProviderConfig,
14
+ } from "actagent/plugin-sdk/provider-model-shared";
15
+ import { normalizeLowercaseStringOrEmpty } from "actagent/plugin-sdk/string-coerce-runtime";
16
+
17
+ const log = createSubsystemLogger("bedrock-mantle-discovery");
18
+
19
+ const DEFAULT_COST = {
20
+ input: 0,
21
+ output: 0,
22
+ cacheRead: 0,
23
+ cacheWrite: 0,
24
+ };
25
+
26
+ const DEFAULT_CONTEXT_WINDOW = 32000;
27
+ const DEFAULT_MAX_TOKENS = 4096;
28
+ const DEFAULT_REFRESH_INTERVAL_SECONDS = 3600; // 1 hour
29
+ /** Config auth marker meaning Mantle should mint runtime bearer tokens from IAM. */
30
+ export const MANTLE_IAM_TOKEN_MARKER = "__amazon_bedrock_mantle_iam__";
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Mantle region & endpoint helpers
34
+ // ---------------------------------------------------------------------------
35
+
36
+ const MANTLE_SUPPORTED_REGIONS = [
37
+ "us-east-1",
38
+ "us-east-2",
39
+ "us-west-2",
40
+ "ap-northeast-1",
41
+ "ap-south-1",
42
+ "ap-southeast-3",
43
+ "eu-central-1",
44
+ "eu-west-1",
45
+ "eu-west-2",
46
+ "eu-south-1",
47
+ "eu-north-1",
48
+ "sa-east-1",
49
+ ] as const;
50
+
51
+ function mantleEndpoint(region: string): string {
52
+ return `https://bedrock-mantle.${region}.api.aws`;
53
+ }
54
+
55
+ function isSupportedRegion(region: string): boolean {
56
+ return (MANTLE_SUPPORTED_REGIONS as readonly string[]).includes(region);
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Bearer token resolution
61
+ // ---------------------------------------------------------------------------
62
+
63
+ type MantleBearerTokenProvider = () => Promise<string>;
64
+ type MantleBearerTokenProviderFactory = (opts?: {
65
+ region?: string;
66
+ expiresInSeconds?: number;
67
+ }) => MantleBearerTokenProvider;
68
+
69
+ async function loadMantleBearerTokenProviderFactory(): Promise<MantleBearerTokenProviderFactory> {
70
+ const { getTokenProvider } = (await import("@aws/bedrock-token-generator")) as {
71
+ getTokenProvider: MantleBearerTokenProviderFactory;
72
+ };
73
+ return getTokenProvider;
74
+ }
75
+
76
+ /**
77
+ * Resolve a bearer token for Mantle authentication.
78
+ *
79
+ * Returns the value of AWS_BEARER_TOKEN_BEDROCK if set, undefined otherwise.
80
+ * When no explicit token is set, `resolveImplicitMantleProvider` will attempt
81
+ * to generate one from IAM credentials via `@aws/bedrock-token-generator`.
82
+ */
83
+ export function resolveMantleBearerToken(env: NodeJS.ProcessEnv = process.env): string | undefined {
84
+ const explicitToken = env.AWS_BEARER_TOKEN_BEDROCK?.trim();
85
+ if (explicitToken) {
86
+ return explicitToken;
87
+ }
88
+ return undefined;
89
+ }
90
+
91
+ /** Token cache for IAM-derived bearer tokens, keyed by region. */
92
+ const iamTokenCache = new Map<string, { token: string; expiresAt: number }>();
93
+ const IAM_TOKEN_TTL_MS = 7200_000; // Matches the 2h token lifetime we request below.
94
+
95
+ function resolveMantleRegion(env: NodeJS.ProcessEnv): string {
96
+ return env.AWS_REGION ?? env.AWS_DEFAULT_REGION ?? "us-east-1";
97
+ }
98
+
99
+ function getCachedIamTokenEntry(
100
+ region: string,
101
+ now: number = Date.now(),
102
+ ): { token: string; expiresAt: number } | undefined {
103
+ const cached = iamTokenCache.get(region);
104
+ if (cached && isFutureDateTimestampMs(cached.expiresAt, { nowMs: now })) {
105
+ return cached;
106
+ }
107
+ iamTokenCache.delete(region);
108
+ return undefined;
109
+ }
110
+
111
+ /**
112
+ * Generate a bearer token from IAM credentials using `@aws/bedrock-token-generator`.
113
+ *
114
+ * Uses the AWS default credential chain (instance roles, SSO, access keys, EKS IRSA).
115
+ * Returns undefined if the package is not installed or credentials are unavailable.
116
+ */
117
+ export async function generateBearerTokenFromIam(params: {
118
+ region: string;
119
+ now?: () => number;
120
+ tokenProviderFactory?: MantleBearerTokenProviderFactory;
121
+ }): Promise<string | undefined> {
122
+ const now = params.now?.() ?? Date.now();
123
+ const cached = getCachedIamTokenEntry(params.region, now);
124
+
125
+ if (cached) {
126
+ return cached.token;
127
+ }
128
+
129
+ try {
130
+ const getTokenProvider =
131
+ params.tokenProviderFactory ?? (await loadMantleBearerTokenProviderFactory());
132
+ const token = await getTokenProvider({
133
+ region: params.region,
134
+ expiresInSeconds: 7200, // 2 hours
135
+ })();
136
+ const expiresAt = resolveExpiresAtMsFromDurationMs(IAM_TOKEN_TTL_MS, { nowMs: now });
137
+ if (expiresAt !== undefined) {
138
+ iamTokenCache.set(params.region, { token, expiresAt });
139
+ }
140
+ return token;
141
+ } catch (error) {
142
+ log.debug?.("Mantle IAM token generation unavailable", {
143
+ region: params.region,
144
+ error: formatErrorMessage(error),
145
+ });
146
+ return undefined;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Read a cached IAM bearer token for the given region (sync, no generation).
152
+ *
153
+ * Returns the token if it exists and has not expired, undefined otherwise.
154
+ * Used by Mantle runtime auth and tests to inspect the current cache.
155
+ */
156
+ export function getCachedIamToken(region: string): string | undefined {
157
+ return getCachedIamTokenEntry(region)?.token;
158
+ }
159
+
160
+ /** Resolve the actual runtime bearer token for Mantle, generating IAM tokens when needed. */
161
+ export async function resolveMantleRuntimeBearerToken(params: {
162
+ apiKey: string;
163
+ env?: NodeJS.ProcessEnv;
164
+ now?: () => number;
165
+ tokenProviderFactory?: MantleBearerTokenProviderFactory;
166
+ }): Promise<{ apiKey: string; expiresAt?: number } | undefined> {
167
+ if (params.apiKey !== MANTLE_IAM_TOKEN_MARKER) {
168
+ return { apiKey: params.apiKey };
169
+ }
170
+ const now = params.now?.() ?? Date.now();
171
+ const region = resolveMantleRegion(params.env ?? process.env);
172
+ const cached = getCachedIamTokenEntry(region, now);
173
+ if (cached) {
174
+ return {
175
+ apiKey: cached.token,
176
+ expiresAt: cached.expiresAt,
177
+ };
178
+ }
179
+ const token = await generateBearerTokenFromIam({
180
+ region,
181
+ now: params.now,
182
+ tokenProviderFactory: params.tokenProviderFactory,
183
+ });
184
+ if (!token) {
185
+ return undefined;
186
+ }
187
+ const refreshed = getCachedIamTokenEntry(region, now);
188
+ const expiresAt =
189
+ refreshed?.expiresAt ?? resolveExpiresAtMsFromDurationMs(IAM_TOKEN_TTL_MS, { nowMs: now });
190
+ return {
191
+ apiKey: refreshed?.token ?? token,
192
+ ...(expiresAt === undefined ? {} : { expiresAt }),
193
+ };
194
+ }
195
+ /** Clear the IAM token cache for tests. */
196
+ export function resetIamTokenCacheForTest(): void {
197
+ iamTokenCache.clear();
198
+ }
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // OpenAI-format model list response
202
+ // ---------------------------------------------------------------------------
203
+
204
+ interface OpenAIModelEntry {
205
+ id: string;
206
+ object?: string;
207
+ owned_by?: string;
208
+ created?: number;
209
+ }
210
+
211
+ interface OpenAIModelsResponse {
212
+ data?: OpenAIModelEntry[];
213
+ object?: string;
214
+ }
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // Reasoning heuristic
218
+ // ---------------------------------------------------------------------------
219
+
220
+ /** Model ID substrings that indicate reasoning/thinking support. */
221
+ const REASONING_PATTERNS = [
222
+ "thinking",
223
+ "reasoner",
224
+ "reasoning",
225
+ "deepseek.r",
226
+ "gpt-oss-120b", // GPT-OSS 120B supports reasoning
227
+ "gpt-oss-safeguard-120b",
228
+ ];
229
+
230
+ function inferReasoningSupport(modelId: string): boolean {
231
+ const lower = normalizeLowercaseStringOrEmpty(modelId);
232
+ return REASONING_PATTERNS.some((p) => lower.includes(p));
233
+ }
234
+
235
+ // ---------------------------------------------------------------------------
236
+ // Discovery cache
237
+ // ---------------------------------------------------------------------------
238
+
239
+ interface MantleCacheEntry {
240
+ models: ModelDefinitionConfig[];
241
+ fetchedAt: number;
242
+ }
243
+
244
+ type MantleDiscoveryConfig = {
245
+ enabled?: boolean;
246
+ };
247
+
248
+ const discoveryCache = new Map<string, MantleCacheEntry>();
249
+
250
+ /** Clear the Mantle discovery cache for tests. */
251
+ export function resetMantleDiscoveryCacheForTest(): void {
252
+ discoveryCache.clear();
253
+ }
254
+
255
+ // ---------------------------------------------------------------------------
256
+ // Model discovery
257
+ // ---------------------------------------------------------------------------
258
+
259
+ /**
260
+ * Discover available models from the Mantle `/v1/models` endpoint.
261
+ *
262
+ * The response is in standard OpenAI format:
263
+ * ```json
264
+ * { "data": [{ "id": "anthropic.claude-sonnet-4-6", "object": "model", "owned_by": "anthropic" }] }
265
+ * ```
266
+ *
267
+ * Results are cached per region for `DEFAULT_REFRESH_INTERVAL_SECONDS`.
268
+ * Returns an empty array if the request fails (no permission, network error, etc.).
269
+ */
270
+ /** Discover Mantle models for one region/config. */
271
+ export async function discoverMantleModels(params: {
272
+ region: string;
273
+ bearerToken: string;
274
+ fetchFn?: typeof fetch;
275
+ now?: () => number;
276
+ }): Promise<ModelDefinitionConfig[]> {
277
+ const { region, bearerToken, fetchFn = fetch, now = Date.now } = params;
278
+
279
+ // Check cache
280
+ const cacheKey = region;
281
+ const cached = discoveryCache.get(cacheKey);
282
+ if (cached && now() - cached.fetchedAt < DEFAULT_REFRESH_INTERVAL_SECONDS * 1000) {
283
+ return cached.models;
284
+ }
285
+
286
+ const endpoint = `${mantleEndpoint(region)}/v1/models`;
287
+
288
+ try {
289
+ const response = await fetchFn(endpoint, {
290
+ method: "GET",
291
+ headers: {
292
+ Authorization: `Bearer ${bearerToken}`,
293
+ Accept: "application/json",
294
+ },
295
+ });
296
+
297
+ if (!response.ok) {
298
+ log.debug?.("Mantle model discovery failed", {
299
+ status: response.status,
300
+ statusText: response.statusText,
301
+ });
302
+ return cached?.models ?? [];
303
+ }
304
+
305
+ const body = (await response.json()) as OpenAIModelsResponse;
306
+ const rawModels = body.data ?? [];
307
+
308
+ const models = rawModels
309
+ .filter((m) => m.id?.trim())
310
+ .map((m) => ({
311
+ id: m.id,
312
+ name: m.id, // Mantle doesn't return display names
313
+ reasoning: inferReasoningSupport(m.id),
314
+ input: ["text" as const],
315
+ cost: DEFAULT_COST,
316
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
317
+ maxTokens: DEFAULT_MAX_TOKENS,
318
+ }))
319
+ .toSorted((a, b) => a.id.localeCompare(b.id));
320
+
321
+ discoveryCache.set(cacheKey, { models, fetchedAt: now() });
322
+ return models;
323
+ } catch (error) {
324
+ log.debug?.("Mantle model discovery error", {
325
+ error: formatErrorMessage(error),
326
+ });
327
+ return cached?.models ?? [];
328
+ }
329
+ }
330
+
331
+ // ---------------------------------------------------------------------------
332
+ // Implicit provider resolution
333
+ // ---------------------------------------------------------------------------
334
+
335
+ /**
336
+ * Resolve an implicit Bedrock Mantle provider if authentication is available.
337
+ *
338
+ * Detection priority:
339
+ * 1. AWS_BEARER_TOKEN_BEDROCK env var → use directly
340
+ * 2. IAM credentials → generate bearer token via `@aws/bedrock-token-generator`
341
+ * - Region from AWS_REGION / AWS_DEFAULT_REGION / default us-east-1
342
+ * - Models discovered from `/v1/models`
343
+ */
344
+ /** Resolve implicit Mantle provider config from env, IAM token support, and discovery. */
345
+ export async function resolveImplicitMantleProvider(params: {
346
+ env?: NodeJS.ProcessEnv;
347
+ pluginConfig?: { discovery?: MantleDiscoveryConfig };
348
+ fetchFn?: typeof fetch;
349
+ tokenProviderFactory?: MantleBearerTokenProviderFactory;
350
+ }): Promise<ModelProviderConfig | null> {
351
+ const env = params.env ?? process.env;
352
+ if (params.pluginConfig?.discovery?.enabled === false) {
353
+ return null;
354
+ }
355
+ const region = resolveMantleRegion(env);
356
+ const explicitBearerToken = resolveMantleBearerToken(env);
357
+
358
+ if (!isSupportedRegion(region)) {
359
+ log.debug?.("Mantle not available in region", { region });
360
+ return null;
361
+ }
362
+
363
+ // Try explicit token first, then generate from IAM credentials
364
+ const bearerToken =
365
+ explicitBearerToken ??
366
+ (await generateBearerTokenFromIam({
367
+ region,
368
+ tokenProviderFactory: params.tokenProviderFactory,
369
+ }));
370
+
371
+ if (!bearerToken) {
372
+ return null;
373
+ }
374
+
375
+ const models = await discoverMantleModels({
376
+ region,
377
+ bearerToken,
378
+ fetchFn: params.fetchFn,
379
+ });
380
+
381
+ if (models.length === 0) {
382
+ return null;
383
+ }
384
+
385
+ log.debug?.("Mantle provider resolved", { region, modelCount: models.length });
386
+
387
+ // Append Claude models available on Mantle's Anthropic Messages endpoint.
388
+ // Opus 4.7 currently needs the provider-owned bearer-auth path here, but we
389
+ // keep reasoning off until the underlying Anthropic transport learns Opus 4.7
390
+ // adaptive thinking semantics.
391
+ const claudeModels: ModelDefinitionConfig[] = [
392
+ {
393
+ id: "anthropic.claude-opus-4-7",
394
+ name: "Claude Opus 4.7",
395
+ api: "anthropic-messages" as const,
396
+ reasoning: false,
397
+ input: ["text", "image"],
398
+ cost: {
399
+ input: 5,
400
+ output: 25,
401
+ cacheRead: 0.5,
402
+ cacheWrite: 6.25,
403
+ },
404
+ contextWindow: 1_000_000,
405
+ maxTokens: 128_000,
406
+ },
407
+ ];
408
+ const allModels = [...models, ...claudeModels];
409
+
410
+ return {
411
+ baseUrl: `${mantleEndpoint(region)}/v1`,
412
+ api: "openai-completions",
413
+ auth: "api-key",
414
+ apiKey: explicitBearerToken ? "env:AWS_BEARER_TOKEN_BEDROCK" : MANTLE_IAM_TOKEN_MARKER,
415
+ models: allModels,
416
+ };
417
+ }
418
+
419
+ /** Merge an implicit Mantle provider catalog with explicit user config. */
420
+ export function mergeImplicitMantleProvider(params: {
421
+ existing: ModelProviderConfig | undefined;
422
+ implicit: ModelProviderConfig;
423
+ }): ModelProviderConfig {
424
+ const { existing, implicit } = params;
425
+ if (!existing) {
426
+ return implicit;
427
+ }
428
+ return {
429
+ ...implicit,
430
+ ...existing,
431
+ models:
432
+ Array.isArray(existing.models) && existing.models.length > 0
433
+ ? existing.models
434
+ : implicit.models,
435
+ };
436
+ }
package/index.test.ts ADDED
@@ -0,0 +1,88 @@
1
+ // Amazon Bedrock Mantle tests cover index plugin behavior.
2
+ import { registerSingleProviderPlugin } from "actagent/plugin-sdk/plugin-test-runtime";
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+ import bedrockMantlePlugin from "./index.js";
5
+
6
+ describe("amazon-bedrock-mantle provider plugin", () => {
7
+ beforeEach(() => {
8
+ vi.restoreAllMocks();
9
+ });
10
+
11
+ it("uses live plugin config to disable catalog discovery", async () => {
12
+ const fetchMock = vi
13
+ .spyOn(globalThis, "fetch")
14
+ .mockRejectedValue(new Error("unexpected fetch"));
15
+ const provider = await registerSingleProviderPlugin(bedrockMantlePlugin);
16
+ const catalog = provider.catalog;
17
+ if (!catalog) {
18
+ throw new Error("catalog registration missing");
19
+ }
20
+
21
+ const result = await catalog.run({
22
+ config: {
23
+ plugins: {
24
+ entries: {
25
+ "amazon-bedrock-mantle": {
26
+ config: {
27
+ discovery: { enabled: false },
28
+ },
29
+ },
30
+ },
31
+ },
32
+ },
33
+ env: {
34
+ AWS_BEARER_TOKEN_BEDROCK: "test-token",
35
+ AWS_REGION: "us-east-1",
36
+ },
37
+ } as never);
38
+
39
+ expect(result).toBeNull();
40
+ expect(fetchMock).not.toHaveBeenCalled();
41
+ });
42
+
43
+ it("registers with correct provider ID and label", async () => {
44
+ const provider = await registerSingleProviderPlugin(bedrockMantlePlugin);
45
+ expect(provider.id).toBe("amazon-bedrock-mantle");
46
+ expect(provider.label).toBe("Amazon Bedrock Mantle (OpenAI-compatible)");
47
+ });
48
+
49
+ it("classifies rate limit errors for failover", async () => {
50
+ const provider = await registerSingleProviderPlugin(bedrockMantlePlugin);
51
+ expect(
52
+ provider.classifyFailoverReason?.({ errorMessage: "rate_limit exceeded" } as never),
53
+ ).toBe("rate_limit");
54
+ expect(
55
+ provider.classifyFailoverReason?.({ errorMessage: "429 Too Many Requests" } as never),
56
+ ).toBe("rate_limit");
57
+ expect(
58
+ provider.classifyFailoverReason?.({ errorMessage: "some other error" } as never),
59
+ ).toBeUndefined();
60
+ expect(provider.classifyFailoverReason?.({ errorMessage: "overloaded_error" } as never)).toBe(
61
+ "overloaded",
62
+ );
63
+ });
64
+
65
+ it("provides a custom stream only for Mantle Anthropic models", async () => {
66
+ const provider = await registerSingleProviderPlugin(bedrockMantlePlugin);
67
+
68
+ expect(
69
+ typeof provider.createStreamFn?.({
70
+ provider: "amazon-bedrock-mantle",
71
+ modelId: "anthropic.claude-opus-4-7",
72
+ model: {
73
+ api: "anthropic-messages",
74
+ },
75
+ } as never),
76
+ ).toBe("function");
77
+
78
+ expect(
79
+ provider.createStreamFn?.({
80
+ provider: "amazon-bedrock-mantle",
81
+ modelId: "openai.gpt-oss-120b",
82
+ model: {
83
+ api: "openai-completions",
84
+ },
85
+ } as never),
86
+ ).toBeUndefined();
87
+ });
88
+ });
package/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Amazon Bedrock Mantle plugin entry. Registers the OpenAI-compatible Mantle
3
+ * provider plus Anthropic stream compatibility hooks.
4
+ */
5
+ import { definePluginEntry } from "actagent/plugin-sdk/plugin-entry";
6
+ import { registerBedrockMantlePlugin } from "./register.sync.runtime.js";
7
+
8
+ export default definePluginEntry({
9
+ id: "amazon-bedrock-mantle",
10
+ name: "Amazon Bedrock Mantle Provider",
11
+ description: "Bundled Amazon Bedrock Mantle (OpenAI-compatible) provider plugin",
12
+ register(api) {
13
+ registerBedrockMantlePlugin(api);
14
+ },
15
+ });
@@ -0,0 +1,123 @@
1
+ // Amazon Bedrock Mantle tests cover mantle anthropic plugin behavior.
2
+ import type { Model } from "actagent/plugin-sdk/llm";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import {
5
+ createMantleAnthropicStreamFn,
6
+ resolveMantleAnthropicBaseUrl,
7
+ } from "./mantle-anthropic.runtime.js";
8
+
9
+ function createTestModel(): Model {
10
+ return {
11
+ id: "anthropic.claude-opus-4-7",
12
+ name: "Claude Opus 4.7",
13
+ provider: "amazon-bedrock-mantle",
14
+ api: "anthropic-messages",
15
+ baseUrl: "https://bedrock-mantle.us-east-1.api.aws/v1",
16
+ headers: {
17
+ "X-Test": "model-header",
18
+ },
19
+ reasoning: false,
20
+ input: ["text", "image"],
21
+ cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
22
+ contextWindow: 1_000_000,
23
+ maxTokens: 128_000,
24
+ } as Model;
25
+ }
26
+
27
+ function createTestDeps() {
28
+ return {
29
+ createClient: vi.fn((options: unknown) => ({ options }) as never),
30
+ stream: vi.fn(),
31
+ };
32
+ }
33
+
34
+ function requireRecord(value: unknown, label: string): Record<string, unknown> {
35
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
36
+ throw new Error(`Expected ${label} to be an object`);
37
+ }
38
+ return value as Record<string, unknown>;
39
+ }
40
+
41
+ function mockCallArg(mock: { mock: { calls: unknown[][] } }, index = 0, argIndex = 0): unknown {
42
+ const call = mock.mock.calls[index];
43
+ if (!call) {
44
+ throw new Error(`expected mock call ${index}`);
45
+ }
46
+ return call[argIndex];
47
+ }
48
+
49
+ function expectFirstStreamCall(
50
+ deps: ReturnType<typeof createTestDeps>,
51
+ model: Model,
52
+ context: unknown,
53
+ ) {
54
+ expect(mockCallArg(deps.stream, 0, 0)).toBe(model);
55
+ expect(mockCallArg(deps.stream, 0, 1)).toBe(context);
56
+ }
57
+
58
+ function firstStreamOptions(deps: ReturnType<typeof createTestDeps>): Record<string, unknown> {
59
+ return requireRecord(mockCallArg(deps.stream, 0, 2), "stream options");
60
+ }
61
+
62
+ describe("createMantleAnthropicStreamFn", () => {
63
+ it("uses authToken bearer auth for Mantle Anthropic requests", () => {
64
+ const stream = { kind: "anthropic-stream" };
65
+ const model = createTestModel();
66
+ const context = { messages: [] };
67
+ const deps = createTestDeps();
68
+ deps.stream.mockReturnValue(stream as never);
69
+
70
+ const result = createMantleAnthropicStreamFn(deps)(model, context, {
71
+ apiKey: "bedrock-bearer-token",
72
+ headers: {
73
+ "X-Caller": "caller-header",
74
+ },
75
+ });
76
+
77
+ expect(result).toBe(stream);
78
+ const clientOptions = requireRecord(mockCallArg(deps.createClient), "client options");
79
+ expect(clientOptions.apiKey).toBeNull();
80
+ expect(clientOptions.authToken).toBe("bedrock-bearer-token");
81
+ expect(clientOptions.baseURL).toBe("https://bedrock-mantle.us-east-1.api.aws/anthropic");
82
+ const defaultHeaders = requireRecord(clientOptions.defaultHeaders, "default headers");
83
+ expect(defaultHeaders.accept).toBe("application/json");
84
+ expect(defaultHeaders["anthropic-beta"]).toBe("fine-grained-tool-streaming-2025-05-14");
85
+ expect(defaultHeaders["X-Test"]).toBe("model-header");
86
+ expect(defaultHeaders["X-Caller"]).toBe("caller-header");
87
+
88
+ expectFirstStreamCall(deps, model, context);
89
+ const streamOptions = firstStreamOptions(deps);
90
+ const client = requireRecord(streamOptions.client, "stream client");
91
+ expect(requireRecord(client.options, "stream client options").authToken).toBe(
92
+ "bedrock-bearer-token",
93
+ );
94
+ expect(streamOptions.thinkingEnabled).toBe(false);
95
+ });
96
+
97
+ it("omits unsupported Opus 4.7 sampling and reasoning overrides", () => {
98
+ const model = createTestModel();
99
+ const context = { messages: [] };
100
+ const deps = createTestDeps();
101
+ deps.stream.mockReturnValue({ kind: "anthropic-stream" } as never);
102
+
103
+ void createMantleAnthropicStreamFn(deps)(model, context, {
104
+ apiKey: "bedrock-bearer-token",
105
+ temperature: 0.2,
106
+ reasoning: "high",
107
+ });
108
+
109
+ expectFirstStreamCall(deps, model, context);
110
+ const streamOptions = firstStreamOptions(deps);
111
+ expect(streamOptions.temperature).toBeUndefined();
112
+ expect(streamOptions.thinkingEnabled).toBe(false);
113
+ });
114
+
115
+ it("normalizes Mantle provider URLs to the Anthropic endpoint", () => {
116
+ expect(resolveMantleAnthropicBaseUrl("https://bedrock-mantle.us-east-1.api.aws/v1")).toBe(
117
+ "https://bedrock-mantle.us-east-1.api.aws/anthropic",
118
+ );
119
+ expect(
120
+ resolveMantleAnthropicBaseUrl("https://bedrock-mantle.us-east-1.api.aws/anthropic/"),
121
+ ).toBe("https://bedrock-mantle.us-east-1.api.aws/anthropic");
122
+ });
123
+ });