@oh-my-pi/pi-ai 17.2.4 → 17.2.6

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/types/dialect/examples.d.ts +18 -2
  3. package/dist/types/dialect/inventory.d.ts +6 -9
  4. package/dist/types/dialect/rendering.d.ts +9 -0
  5. package/dist/types/dialect/types.d.ts +0 -1
  6. package/dist/types/error/aws.d.ts +5 -1
  7. package/dist/types/error/flags.d.ts +5 -0
  8. package/dist/types/providers/aws-credentials.d.ts +5 -10
  9. package/dist/types/providers/bedrock-mantle.d.ts +13 -0
  10. package/dist/types/providers/openai-shared.d.ts +17 -5
  11. package/dist/types/providers/transform-messages.d.ts +1 -1
  12. package/dist/types/registry/amazon-bedrock.d.ts +7 -1
  13. package/dist/types/registry/aws.d.ts +13 -0
  14. package/dist/types/registry/bedrock-mantle.d.ts +22 -0
  15. package/dist/types/registry/registry.d.ts +25 -1
  16. package/dist/types/registry/types.d.ts +24 -0
  17. package/dist/types/types.d.ts +11 -4
  18. package/dist/types/utils/aws-profile.d.ts +17 -0
  19. package/dist/types/utils/harmony-leak.d.ts +9 -0
  20. package/dist/types/utils/schema/typescript.d.ts +8 -2
  21. package/package.json +4 -4
  22. package/src/auth-broker/discover.ts +2 -1
  23. package/src/auth-broker/wire-schema-resource.ts +6 -1
  24. package/src/auth-storage.ts +2 -2
  25. package/src/dialect/examples.ts +50 -12
  26. package/src/dialect/gemini.ts +17 -31
  27. package/src/dialect/harmony.ts +1 -2
  28. package/src/dialect/inventory.ts +21 -64
  29. package/src/dialect/rendering.ts +54 -0
  30. package/src/dialect/types.ts +0 -1
  31. package/src/error/aws.ts +5 -1
  32. package/src/error/flags.ts +8 -0
  33. package/src/providers/amazon-bedrock.ts +5 -5
  34. package/src/providers/anthropic.ts +77 -32
  35. package/src/providers/aws-credentials.ts +262 -76
  36. package/src/providers/bedrock-mantle.ts +110 -0
  37. package/src/providers/cursor.ts +6 -3
  38. package/src/providers/ollama.ts +27 -1
  39. package/src/providers/openai-codex-responses.ts +19 -5
  40. package/src/providers/openai-shared.ts +84 -35
  41. package/src/providers/transform-messages.ts +1 -1
  42. package/src/registry/amazon-bedrock.ts +9 -14
  43. package/src/registry/aws.ts +57 -0
  44. package/src/registry/bedrock-mantle.ts +34 -0
  45. package/src/registry/google-vertex.ts +2 -2
  46. package/src/registry/registry.ts +2 -0
  47. package/src/registry/types.ts +30 -0
  48. package/src/stream.ts +46 -30
  49. package/src/types.ts +14 -8
  50. package/src/utils/aws-profile.ts +88 -0
  51. package/src/utils/harmony-leak.ts +12 -0
  52. package/src/utils/schema/typescript.ts +21 -7
  53. package/src/utils.ts +1 -1
@@ -84,6 +84,7 @@ import type {
84
84
  ResponseFunctionToolCall,
85
85
  ResponseInput,
86
86
  ResponseInputContent,
87
+ ResponseOutputItem,
87
88
  ResponseOutputMessage,
88
89
  ResponseReasoningItem,
89
90
  ResponseStatus,
@@ -96,6 +97,7 @@ import {
96
97
  appendReasoningSummaryPart,
97
98
  appendReasoningSummaryPartDone,
98
99
  appendReasoningSummaryTextDelta,
100
+ appendResponsesImageResult,
99
101
  appendResponsesToolResultMessages,
100
102
  applyOpenAIServiceTier,
101
103
  applyReasoningSummaryDone,
@@ -106,7 +108,7 @@ import {
106
108
  createSequentialCutoffSummaryState,
107
109
  encodeResponsesToolCallId,
108
110
  encodeTextSignatureV1,
109
- escapeReplayedClientText,
111
+ escapeReplayedControlTokens,
110
112
  finalizeCustomToolCallInputDone,
111
113
  finalizeMessageText,
112
114
  finalizePendingResponsesToolCalls,
@@ -371,7 +373,8 @@ type CodexEventItem =
371
373
  | ResponseOutputMessage
372
374
  | ResponseFunctionToolCall
373
375
  | ResponseCustomToolCall
374
- | ResponseComputerToolCall;
376
+ | ResponseComputerToolCall
377
+ | ResponseOutputItem.ImageGenerationCall;
375
378
  type CodexOutputBlock =
376
379
  | ThinkingContent
377
380
  | TextContent
@@ -2289,6 +2292,7 @@ class CodexStreamProcessor {
2289
2292
  const rawItem = rawEvent.item;
2290
2293
  if (!rawItem || typeof rawItem !== "object") return;
2291
2294
  const item = structuredCloneJSON(rawItem) as CodexEventItem;
2295
+ if (item.type === "image_generation_call" && item.result) item.status = "completed";
2292
2296
  runtime.nativeOutputItems.push(item as unknown as Record<string, unknown>);
2293
2297
 
2294
2298
  // Match the finalization to the OPEN ITEM that started this block, not the
@@ -2301,6 +2305,12 @@ class CodexStreamProcessor {
2301
2305
  const block = entry?.block ?? null;
2302
2306
  const contentIndex = entry?.contentIndex ?? output.content.length - 1;
2303
2307
 
2308
+ if (item.type === "image_generation_call" && item.result) {
2309
+ appendResponsesImageResult(output, stream, item.result);
2310
+ runtime.closeOpenItem(entry);
2311
+ return;
2312
+ }
2313
+
2304
2314
  if (item.type === "reasoning" && block?.type === "thinking") {
2305
2315
  this.#flushSummaryDeltas(entry);
2306
2316
  block.thinking = finalizeReasoningThinking(
@@ -4339,6 +4349,9 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
4339
4349
  };
4340
4350
 
4341
4351
  const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
4352
+ // gpt-5.x reject raw Harmony control-token spellings anywhere in replayed
4353
+ // input, including the model's own tool-call arguments (#6913).
4354
+ const escapeControlTokens = isHarmonyDialectModel(model);
4342
4355
  let msgIndex = 0;
4343
4356
  // Track call_ids that originated as custom tool calls so paired tool-result
4344
4357
  // messages can be replayed as `custom_tool_call_output` rather than
@@ -4368,7 +4381,7 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
4368
4381
  knownCallIds.add(item.call_id);
4369
4382
  }
4370
4383
  }
4371
- messages.push(...(isHarmonyDialectModel(model) ? escapeReplayedClientText(replayItems) : replayItems));
4384
+ messages.push(...(escapeControlTokens ? escapeReplayedControlTokens(replayItems) : replayItems));
4372
4385
  msgIndex += 1;
4373
4386
  continue;
4374
4387
  }
@@ -4394,10 +4407,11 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
4394
4407
  if (historyItems) {
4395
4408
  const sanitizedHistoryItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(historyItems);
4396
4409
  if (sanitizedHistoryItems) {
4397
- const replayItems =
4410
+ const rawReplayItems =
4398
4411
  model.supportsComputerUse === true
4399
4412
  ? sanitizedHistoryItems
4400
4413
  : unrollCodexComputerItems(sanitizedHistoryItems, model.compat.supportsImageDetailOriginal);
4414
+ const replayItems = escapeControlTokens ? escapeReplayedControlTokens(rawReplayItems) : rawReplayItems;
4401
4415
  for (const item of replayItems) {
4402
4416
  if (item.type === "custom_tool_call") {
4403
4417
  customCallIds.add(item.call_id);
@@ -4435,7 +4449,7 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
4435
4449
  ? sanitizeOpenAIResponsesAssistantFallbackItemsForReplay(convertedOutputItems)
4436
4450
  : convertedOutputItems;
4437
4451
  if (outputItems.length > 0) {
4438
- messages.push(...outputItems);
4452
+ messages.push(...(escapeControlTokens ? escapeReplayedControlTokens(outputItems) : outputItems));
4439
4453
  }
4440
4454
  msgIndex += 1;
4441
4455
  continue;
@@ -78,7 +78,11 @@ import {
78
78
  kStreamingPartialJson,
79
79
  } from "../utils/block-symbols";
80
80
  import type { AssistantMessageEventStream } from "../utils/event-stream";
81
- import { escapeHarmonyControlTokens, isHarmonyDialectModel } from "../utils/harmony-leak";
81
+ import {
82
+ escapeHarmonyControlTokens,
83
+ escapeHarmonyControlTokensInJson,
84
+ isHarmonyDialectModel,
85
+ } from "../utils/harmony-leak";
82
86
  import type { CapturedHttpErrorResponse } from "../utils/http-inspector";
83
87
  import { getOpenRouterHeaders } from "../utils/openrouter-headers";
84
88
  import { isForcedToolChoice } from "../utils/tool-choice";
@@ -329,9 +333,7 @@ export function applyOpenAIServiceTier(
329
333
  model: Pick<Model, "provider" | "api" | "id">,
330
334
  ): void {
331
335
  if (!shouldSendServiceTier(serviceTier, model)) return;
332
- if (serviceTier === "flex" || serviceTier === "scale" || serviceTier === "priority") {
333
- params.service_tier = serviceTier;
334
- }
336
+ params.service_tier = serviceTier;
335
337
  }
336
338
 
337
339
  /**
@@ -1636,39 +1638,71 @@ export interface BuildResponsesInputOptions<TApi extends Api> {
1636
1638
  }
1637
1639
 
1638
1640
  /**
1639
- * Escape reserved Harmony control tokens in the client-boundary text of
1640
- * replayed Responses input items — user/developer/system message text and
1641
- * tool-result output. Model-owned items (assistant output, reasoning, tool-call
1642
- * arguments) carry no client data and are returned untouched.
1641
+ * Escape reserved Harmony control tokens in the free-text fields of replayed
1642
+ * Responses input items: user/developer/system text, tool-result output,
1643
+ * assistant message text, and tool-call payloads.
1644
+ *
1645
+ * Tool-call items are covered deliberately. The original #6913 fix skipped
1646
+ * model-owned items on the theory that they carry no client data — but a model
1647
+ * legitimately writing *about* Harmony samples `<|channel|>` etc. into its own
1648
+ * `function_call.arguments`, and a full-transcript replay (stale or blocked
1649
+ * previous_response_id, provider fallback) feeds those bytes back as input,
1650
+ * which gpt-5.x reject with invalid_prompt / "Request blocked", permanently
1651
+ * poisoning the session. `arguments` is a JSON document, so it uses
1652
+ * {@link escapeHarmonyControlTokensInJson} to stay parseable. Reasoning items
1653
+ * are left untouched: `encrypted_content` is opaque and plaintext summaries
1654
+ * are never rendered back into the prompt.
1643
1655
  *
1644
1656
  * Native history replay pushes stored `providerPayload` items straight onto the
1645
1657
  * wire, bypassing {@link convertResponsesInputContent}; without this a stored
1646
1658
  * `input_text` carrying `<|channel|>analysis` still reaches gpt-5.x raw (#6913).
1647
1659
  * Callers gate on {@link isHarmonyDialectModel}. Items are copied, not mutated.
1648
1660
  */
1649
- export function escapeReplayedClientText(items: ResponseInput): ResponseInput {
1661
+ export function escapeReplayedControlTokens(items: ResponseInput): ResponseInput {
1650
1662
  return items.map(item => {
1651
1663
  if (item.type === "function_call_output" || item.type === "custom_tool_call_output") {
1652
1664
  return typeof item.output === "string" ? { ...item, output: escapeHarmonyControlTokens(item.output) } : item;
1653
1665
  }
1666
+ if (item.type === "function_call") {
1667
+ return typeof item.arguments === "string"
1668
+ ? { ...item, arguments: escapeHarmonyControlTokensInJson(item.arguments) }
1669
+ : item;
1670
+ }
1671
+ if (item.type === "custom_tool_call") {
1672
+ return typeof item.input === "string" ? { ...item, input: escapeHarmonyControlTokens(item.input) } : item;
1673
+ }
1654
1674
  // EasyInputMessage may omit `type` (`{ role, content }`); the responses
1655
1675
  // server persists it verbatim, so treat missing type as a message too.
1656
1676
  const isTypedMessage = item.type === "message" || item.type === undefined;
1657
- if (isTypedMessage && "role" in item && "content" in item) {
1658
- const role = item.role;
1659
- if (role !== "user" && role !== "developer" && role !== "system") return item;
1660
- const content = item.content;
1661
- if (typeof content === "string") {
1662
- return { ...item, content: escapeHarmonyControlTokens(content) };
1663
- }
1664
- if (Array.isArray(content)) {
1677
+ if (!isTypedMessage || !("role" in item) || !("content" in item)) return item;
1678
+ if (item.role === "assistant") {
1679
+ // Assistant output text is model-owned but equally capable of carrying
1680
+ // control tokens as data. `status` discriminates ResponseOutputMessage.
1681
+ if ("status" in item && Array.isArray(item.content)) {
1665
1682
  return {
1666
1683
  ...item,
1667
- content: content.map(part =>
1668
- part.type === "input_text" ? { ...part, text: escapeHarmonyControlTokens(part.text) } : part,
1684
+ content: item.content.map(part =>
1685
+ part.type === "output_text"
1686
+ ? { ...part, text: escapeHarmonyControlTokens(part.text) }
1687
+ : part.type === "refusal"
1688
+ ? { ...part, refusal: escapeHarmonyControlTokens(part.refusal) }
1689
+ : part,
1669
1690
  ),
1670
1691
  };
1671
1692
  }
1693
+ return item;
1694
+ }
1695
+ const content = item.content;
1696
+ if (typeof content === "string") {
1697
+ return { ...item, content: escapeHarmonyControlTokens(content) };
1698
+ }
1699
+ if (Array.isArray(content)) {
1700
+ return {
1701
+ ...item,
1702
+ content: content.map(part =>
1703
+ part.type === "input_text" ? { ...part, text: escapeHarmonyControlTokens(part.text) } : part,
1704
+ ),
1705
+ };
1672
1706
  }
1673
1707
  return item;
1674
1708
  });
@@ -1733,7 +1767,7 @@ export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInp
1733
1767
  customToolWireNameMap,
1734
1768
  options.model.supportsComputerUse === true,
1735
1769
  );
1736
- messages.push(...(escapeControlTokens ? escapeReplayedClientText(replayItems) : replayItems));
1770
+ messages.push(...(escapeControlTokens ? escapeReplayedControlTokens(replayItems) : replayItems));
1737
1771
  knownCallIds = collectKnownCallIds(messages);
1738
1772
  for (const id of collectCustomCallIds(messages)) customCallIds.add(id);
1739
1773
  for (const id of collectComputerCallIds(messages)) computerCallIds.add(id);
@@ -1794,10 +1828,16 @@ export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInp
1794
1828
  )
1795
1829
  : undefined;
1796
1830
  if (nativeReplayEnabled && sanitizedHistoryItems) {
1831
+ // Model-owned replay items can carry reserved control-token
1832
+ // spellings as data (the model writing *about* Harmony); escape the
1833
+ // transport copy just like client turns.
1834
+ const wireItems = escapeControlTokens
1835
+ ? escapeReplayedControlTokens(sanitizedHistoryItems)
1836
+ : sanitizedHistoryItems;
1797
1837
  if (providerPayload?.dt) {
1798
- messages.push(...sanitizedHistoryItems);
1838
+ messages.push(...wireItems);
1799
1839
  } else {
1800
- messages.splice(0, messages.length, ...sanitizedHistoryItems);
1840
+ messages.splice(0, messages.length, ...wireItems);
1801
1841
  customCallIds.clear();
1802
1842
  computerCallIds.clear();
1803
1843
  }
@@ -1826,7 +1866,7 @@ export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInp
1826
1866
  ? sanitizeOpenAIResponsesAssistantFallbackItemsForReplay(convertedOutputItems)
1827
1867
  : convertedOutputItems;
1828
1868
  if (outputItems.length === 0) continue;
1829
- messages.push(...outputItems);
1869
+ messages.push(...(escapeControlTokens ? escapeReplayedControlTokens(outputItems) : outputItems));
1830
1870
  } else if (msg.role === "toolResult") {
1831
1871
  appendResponsesToolResultMessages(
1832
1872
  messages,
@@ -2419,6 +2459,26 @@ export function computerCallMetadata(item: ResponseComputerToolCall): ComputerTo
2419
2459
  };
2420
2460
  }
2421
2461
 
2462
+ /** Append a native Responses image result and emit its completion event. */
2463
+ export function appendResponsesImageResult(
2464
+ output: AssistantMessage,
2465
+ stream: AssistantMessageEventStream,
2466
+ result: string,
2467
+ ): void {
2468
+ const image: ImageContent = {
2469
+ type: "image",
2470
+ data: result,
2471
+ mimeType: parseImageMetadata(Buffer.from(result, "base64"))?.mimeType ?? "image/png",
2472
+ };
2473
+ output.content.push(image);
2474
+ stream.push({
2475
+ type: "image_end",
2476
+ contentIndex: output.content.length - 1,
2477
+ content: image,
2478
+ partial: output,
2479
+ });
2480
+ }
2481
+
2422
2482
  export async function processResponsesStream<TApi extends Api>(
2423
2483
  openaiStream: AsyncIterable<ResponseStreamEvent>,
2424
2484
  output: AssistantMessage,
@@ -2932,18 +2992,7 @@ export async function processResponsesStream<TApi extends Api>(
2932
2992
  closeOpenItem(event.output_index, item.id, entry, item.call_id, prefixedFunctionCallItemKey(item.call_id));
2933
2993
  stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
2934
2994
  } else if (item.type === "image_generation_call" && item.status === "completed" && item.result) {
2935
- const image: ImageContent = {
2936
- type: "image",
2937
- data: item.result,
2938
- mimeType: parseImageMetadata(Buffer.from(item.result, "base64"))?.mimeType ?? "image/png",
2939
- };
2940
- output.content.push(image);
2941
- stream.push({
2942
- type: "image_end",
2943
- contentIndex: output.content.length - 1,
2944
- content: image,
2945
- partial: output,
2946
- });
2995
+ appendResponsesImageResult(output, stream, item.result);
2947
2996
  }
2948
2997
  } else if (terminalEvent) {
2949
2998
  const response = terminalEvent.response;
@@ -300,7 +300,7 @@ function normalizeAnthropicTargetToolCallId<TApi extends Api>(
300
300
  * credential redaction is enabled. Exported so hosts can route the same shapes
301
301
  * through reversible obfuscation (keyed placeholders restored before local tool
302
302
  * execution) instead of the irreversible `[*_token_redacted]` rewrite below —
303
- * an irreversible placeholder echoed back in edit-tool `old_text` can never
303
+ * an irreversible placeholder echoed back in edit-tool `old_string` can never
304
304
  * match the real bytes on disk.
305
305
  */
306
306
  export const SENSITIVE_TOKEN_RE =
@@ -1,22 +1,17 @@
1
- import { $env } from "@oh-my-pi/pi-utils";
1
+ import { type AwsBedrockProviderOptions, resolveAwsRegistryApiKey } from "./aws";
2
2
  import type { ProviderDefinition } from "./types";
3
3
 
4
4
  export const amazonBedrockProvider = {
5
5
  id: "amazon-bedrock",
6
6
  name: "Amazon Bedrock",
7
7
  // Amazon Bedrock accepts bearer tokens, IAM keys, profiles, ECS/IRSA credential chains.
8
- envKeys: () => {
9
- const hasEcsCredentials =
10
- !!$env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || !!$env.AWS_CONTAINER_CREDENTIALS_FULL_URI;
11
- const hasWebIdentity = !!$env.AWS_WEB_IDENTITY_TOKEN_FILE && !!$env.AWS_ROLE_ARN;
12
- if (
13
- $env.AWS_PROFILE ||
14
- ($env.AWS_ACCESS_KEY_ID && $env.AWS_SECRET_ACCESS_KEY) ||
15
- $env.AWS_BEARER_TOKEN_BEDROCK ||
16
- hasEcsCredentials ||
17
- hasWebIdentity
18
- ) {
19
- return "<authenticated>";
20
- }
8
+ envKeys: resolveAwsRegistryApiKey,
9
+ mapSimpleOptions: options => {
10
+ const awsOptions = options.providerOptions as AwsBedrockProviderOptions | undefined;
11
+ return {
12
+ region: awsOptions?.region,
13
+ profile: awsOptions?.profile,
14
+ bearerToken: awsOptions?.bearerToken,
15
+ };
21
16
  },
22
17
  } as const satisfies ProviderDefinition;
@@ -0,0 +1,57 @@
1
+ import * as fs from "node:fs";
2
+ import { $env } from "@oh-my-pi/pi-utils";
3
+ import { hasConfiguredAwsProfile } from "../utils/aws-profile";
4
+ import { AUTHENTICATED_SENTINEL } from "./types";
5
+
6
+ export interface AwsBedrockProviderOptions extends Readonly<Record<string, unknown>> {
7
+ /** AWS region used in the service endpoint and SigV4 credential scope. */
8
+ region?: string;
9
+ /** Named AWS shared-credentials/config profile. */
10
+ profile?: string;
11
+ /** Amazon Bedrock API key sent as a bearer token, ahead of SigV4 credential resolution. */
12
+ bearerToken?: string;
13
+ }
14
+
15
+ function isEc2Host(): boolean {
16
+ for (const candidate of [
17
+ "/sys/hypervisor/uuid",
18
+ "/sys/devices/virtual/dmi/id/product_uuid",
19
+ "/sys/devices/virtual/dmi/id/board_asset_tag",
20
+ ]) {
21
+ try {
22
+ const value = fs.readFileSync(candidate, "utf8").trim().toLowerCase();
23
+ if (value.startsWith("ec2")) return true;
24
+ } catch {
25
+ // Missing/unreadable DMI metadata means this probe is inconclusive.
26
+ }
27
+ }
28
+ return false;
29
+ }
30
+
31
+ export function hasAwsCredentialSource(): boolean {
32
+ const hasEcsCredentials = !!$env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || !!$env.AWS_CONTAINER_CREDENTIALS_FULL_URI;
33
+ const hasWebIdentity = !!$env.AWS_WEB_IDENTITY_TOKEN_FILE && !!$env.AWS_ROLE_ARN;
34
+ const hasProfile = hasConfiguredAwsProfile();
35
+ const hasInstanceRole =
36
+ $env.AWS_EC2_METADATA_DISABLED?.toLowerCase() !== "true" &&
37
+ (!!$env.AWS_EC2_METADATA_SERVICE_ENDPOINT || isEc2Host());
38
+ return !!(
39
+ ($env.AWS_ACCESS_KEY_ID && $env.AWS_SECRET_ACCESS_KEY) ||
40
+ $env.AWS_BEARER_TOKEN_BEDROCK ||
41
+ hasWebIdentity ||
42
+ hasProfile ||
43
+ hasEcsCredentials ||
44
+ hasInstanceRole
45
+ );
46
+ }
47
+
48
+ /** Registry key marker for AWS transports that resolve their own bearer/IAM credentials. */
49
+ export function resolveAwsRegistryApiKey(): string | undefined {
50
+ return hasAwsCredentialSource() ? AUTHENTICATED_SENTINEL : undefined;
51
+ }
52
+
53
+ /** Resolve a real AWS bearer token while filtering the registry's auth marker. */
54
+ export function resolveAwsBearerToken(apiKey?: string, bearerToken?: string): string | undefined {
55
+ const resolvedApiKey = apiKey === AUTHENTICATED_SENTINEL ? undefined : apiKey;
56
+ return bearerToken || resolvedApiKey || $env.AWS_BEARER_TOKEN_BEDROCK;
57
+ }
@@ -0,0 +1,34 @@
1
+ import {
2
+ type BedrockMantleOptions,
3
+ createBedrockMantleAuthenticatedFetch,
4
+ prepareBedrockMantleRequest,
5
+ } from "../providers/bedrock-mantle";
6
+ import type { Model } from "../types";
7
+ import { resolveAwsRegion } from "../utils/aws-profile";
8
+ import { resolveAwsBearerToken, resolveAwsRegistryApiKey } from "./aws";
9
+ import type { ProviderDefinition } from "./types";
10
+
11
+ export const bedrockMantleProvider = {
12
+ id: "bedrock-mantle",
13
+ name: "Amazon Bedrock Mantle",
14
+ envKeys: resolveAwsRegistryApiKey,
15
+ allowsMissingApiKey: true,
16
+ prepareRequest: (model, options) =>
17
+ prepareBedrockMantleRequest(model as Model<"openai-responses">, options as BedrockMantleOptions),
18
+ mapSimpleOptions: options => ({ providerOptions: options.providerOptions }),
19
+ prepareModelDiscovery: config => {
20
+ const bearerToken = resolveAwsBearerToken(config.apiKey);
21
+ if (!bearerToken) {
22
+ return { ...config, apiKey: undefined, authenticated: false };
23
+ }
24
+ const region = resolveAwsRegion();
25
+ return {
26
+ authenticated: true,
27
+ baseUrl: `https://bedrock-mantle.${encodeURIComponent(region)}.api.aws/openai/v1`,
28
+ fetch: createBedrockMantleAuthenticatedFetch({
29
+ fetch: config.fetch,
30
+ providerOptions: { bearerToken, region },
31
+ }),
32
+ };
33
+ },
34
+ } as const satisfies ProviderDefinition;
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { $env } from "@oh-my-pi/pi-utils";
5
- import type { ProviderDefinition } from "./types";
5
+ import { AUTHENTICATED_SENTINEL, type ProviderDefinition } from "./types";
6
6
 
7
7
  let cachedVertexAdcCredentialsExists: boolean | null = null;
8
8
 
@@ -32,7 +32,7 @@ export const googleVertexProvider = {
32
32
  const hasProject = !!($env.GOOGLE_CLOUD_PROJECT || $env.GCP_PROJECT || $env.GCLOUD_PROJECT);
33
33
  const hasLocation = !!($env.GOOGLE_VERTEX_LOCATION || $env.GOOGLE_CLOUD_LOCATION || $env.VERTEX_LOCATION);
34
34
  if (hasCredentials && hasProject && hasLocation) {
35
- return "<authenticated>";
35
+ return AUTHENTICATED_SENTINEL;
36
36
  }
37
37
  },
38
38
  } as const satisfies ProviderDefinition;
@@ -7,6 +7,7 @@ import { amazonBedrockProvider } from "./amazon-bedrock";
7
7
  import { anthropicProvider } from "./anthropic";
8
8
  import { azureProvider } from "./azure";
9
9
  import { basetenProvider } from "./baseten";
10
+ import { bedrockMantleProvider } from "./bedrock-mantle";
10
11
  import { cerebrasProvider } from "./cerebras";
11
12
  import { cloudflareAiGatewayProvider } from "./cloudflare-ai-gateway";
12
13
  import { coreWeaveProvider } from "./coreweave";
@@ -157,6 +158,7 @@ const ALL = [
157
158
  mistralProvider,
158
159
  minimaxProvider,
159
160
  amazonBedrockProvider,
161
+ bedrockMantleProvider,
160
162
  gmiCloudProvider,
161
163
  ];
162
164
 
@@ -9,6 +9,8 @@
9
9
  * (default model, model-manager factory, catalog discovery) lives in
10
10
  * `@oh-my-pi/pi-catalog`'s descriptor table.
11
11
  */
12
+
13
+ import type { Api, FetchImpl, Model, SimpleStreamOptions, StreamOptions } from "../types";
12
14
  import type { OAuthCredentials, OAuthLoginCallbacks } from "./oauth/types";
13
15
 
14
16
  /**
@@ -18,6 +20,26 @@ import type { OAuthCredentials, OAuthLoginCallbacks } from "./oauth/types";
18
20
  */
19
21
  export type KeyResolver = string | (() => string | undefined);
20
22
 
23
+ /** Credentials are resolved by the provider transport rather than used as a bearer string. */
24
+ export const AUTHENTICATED_SENTINEL = "<authenticated>";
25
+
26
+ export interface PreparedProviderRequest {
27
+ readonly model: Model<Api>;
28
+ readonly options: StreamOptions;
29
+ }
30
+
31
+ export type ProviderRequestPreparer = (model: Model<Api>, options: StreamOptions) => PreparedProviderRequest;
32
+ export type ProviderSimpleOptionsMapper = (options: SimpleStreamOptions) => Readonly<Record<string, unknown>>;
33
+
34
+ export interface ProviderModelDiscoveryConfig {
35
+ readonly apiKey?: string;
36
+ readonly baseUrl?: string;
37
+ readonly fetch?: FetchImpl;
38
+ readonly authenticated?: boolean;
39
+ }
40
+
41
+ export type ProviderModelDiscoveryPreparer = (config: ProviderModelDiscoveryConfig) => ProviderModelDiscoveryConfig;
42
+
21
43
  /**
22
44
  * Declarative description of a single provider's auth/login wiring. All
23
45
  * fields are optional except `id`/`name`; presence of a field opts the
@@ -42,6 +64,14 @@ export interface ProviderDefinition {
42
64
  readonly showInLoginList?: boolean;
43
65
  // --- env-var fallback (the catalog table's `envVars` supplies plain names; set this only for computed resolvers) ---
44
66
  readonly envKeys?: KeyResolver;
67
+ /** Provider transport can authenticate without a resolved API-key string. */
68
+ readonly allowsMissingApiKey?: boolean;
69
+ /** Provider-owned request shaping applied before generic API dispatch. */
70
+ readonly prepareRequest?: ProviderRequestPreparer;
71
+ /** Provider-owned projection from the generic simple-stream option bag. */
72
+ readonly mapSimpleOptions?: ProviderSimpleOptionsMapper;
73
+ /** Provider-owned authentication and endpoint setup for model discovery. */
74
+ readonly prepareModelDiscovery?: ProviderModelDiscoveryPreparer;
45
75
  // --- interactive login (OAuthProviderInterface-compatible) ---
46
76
  readonly login?: (callbacks: OAuthLoginCallbacks) => Promise<OAuthCredentials | string>;
47
77
  readonly refreshToken?: (credentials: OAuthCredentials) => Promise<OAuthCredentials>;