@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.
- package/CHANGELOG.md +25 -0
- package/dist/types/dialect/examples.d.ts +18 -2
- package/dist/types/dialect/inventory.d.ts +6 -9
- package/dist/types/dialect/rendering.d.ts +9 -0
- package/dist/types/dialect/types.d.ts +0 -1
- package/dist/types/error/aws.d.ts +5 -1
- package/dist/types/error/flags.d.ts +5 -0
- package/dist/types/providers/aws-credentials.d.ts +5 -10
- package/dist/types/providers/bedrock-mantle.d.ts +13 -0
- package/dist/types/providers/openai-shared.d.ts +17 -5
- package/dist/types/providers/transform-messages.d.ts +1 -1
- package/dist/types/registry/amazon-bedrock.d.ts +7 -1
- package/dist/types/registry/aws.d.ts +13 -0
- package/dist/types/registry/bedrock-mantle.d.ts +22 -0
- package/dist/types/registry/registry.d.ts +25 -1
- package/dist/types/registry/types.d.ts +24 -0
- package/dist/types/types.d.ts +11 -4
- package/dist/types/utils/aws-profile.d.ts +17 -0
- package/dist/types/utils/harmony-leak.d.ts +9 -0
- package/dist/types/utils/schema/typescript.d.ts +8 -2
- package/package.json +4 -4
- package/src/auth-broker/discover.ts +2 -1
- package/src/auth-broker/wire-schema-resource.ts +6 -1
- package/src/auth-storage.ts +2 -2
- package/src/dialect/examples.ts +50 -12
- package/src/dialect/gemini.ts +17 -31
- package/src/dialect/harmony.ts +1 -2
- package/src/dialect/inventory.ts +21 -64
- package/src/dialect/rendering.ts +54 -0
- package/src/dialect/types.ts +0 -1
- package/src/error/aws.ts +5 -1
- package/src/error/flags.ts +8 -0
- package/src/providers/amazon-bedrock.ts +5 -5
- package/src/providers/anthropic.ts +77 -32
- package/src/providers/aws-credentials.ts +262 -76
- package/src/providers/bedrock-mantle.ts +110 -0
- package/src/providers/cursor.ts +6 -3
- package/src/providers/ollama.ts +27 -1
- package/src/providers/openai-codex-responses.ts +19 -5
- package/src/providers/openai-shared.ts +84 -35
- package/src/providers/transform-messages.ts +1 -1
- package/src/registry/amazon-bedrock.ts +9 -14
- package/src/registry/aws.ts +57 -0
- package/src/registry/bedrock-mantle.ts +34 -0
- package/src/registry/google-vertex.ts +2 -2
- package/src/registry/registry.ts +2 -0
- package/src/registry/types.ts +30 -0
- package/src/stream.ts +46 -30
- package/src/types.ts +14 -8
- package/src/utils/aws-profile.ts +88 -0
- package/src/utils/harmony-leak.ts +12 -0
- package/src/utils/schema/typescript.ts +21 -7
- 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
|
-
|
|
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(...(
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
1640
|
-
*
|
|
1641
|
-
*
|
|
1642
|
-
*
|
|
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
|
|
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
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
if (
|
|
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 === "
|
|
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 ?
|
|
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(...
|
|
1838
|
+
messages.push(...wireItems);
|
|
1799
1839
|
} else {
|
|
1800
|
-
messages.splice(0, messages.length, ...
|
|
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
|
-
|
|
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 `
|
|
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 {
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
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
|
|
35
|
+
return AUTHENTICATED_SENTINEL;
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
} as const satisfies ProviderDefinition;
|
package/src/registry/registry.ts
CHANGED
|
@@ -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
|
|
package/src/registry/types.ts
CHANGED
|
@@ -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>;
|